package main import ( "crypto/rsa" "crypto/x509" "encoding/json" "encoding/pem" "flag" "fmt" "io/ioutil" "log" "net/http" "os" "strings" ) func mustGetEnv(env string) (val string) { val, ok := os.LookupEnv(env) if !ok { log.Fatalf("required env var %s not found", env) } return } var host string type appDef struct { Name string WebhookURL string PublicKey *rsa.PublicKey } var apps map[string]appDef type profileLink struct { Rel string `json:"rel"` Type string `json:"type"` Href string `json:"href"` } type profileWebFingerData struct { Subject string `json:"subject"` Links []profileLink `json:"links"` } type pubKeyBlock struct { ID string `json:"id"` Owner string `json:"owner"` PEM string `json:"publicKeyPem"` } type profilePage struct { Context []string `json:"@context"` ID string `json:"id"` Type string `json:"type"` PreferredUsername string `json:"preferredUsername"` Name string `json:"name"` Inbox string `json:"inbox"` Followers string `json:"followers"` PublicKey *pubKeyBlock `json:"publicKey,omitempty"` } func main() { prefix := flag.String("prefix", "AW_", "Environment var prefix") proto := flag.String("proto", "https", "Protocol under which this server is going to be exposed") flag.Parse() bind, ok := os.LookupEnv(*prefix + "BIND") if !ok { bind = ":8080" // Default to *:8080 } host = strings.ToLower(mustGetEnv(*prefix + "HOST")) fullhost := fmt.Sprintf("%s://%s", *proto, host) // Find app definitions envs := os.Environ() envAppPrefix := *prefix + "APP_" envAppPrefixLen := len(envAppPrefix) apps = make(map[string]appDef) for _, envName := range envs { // Filter for APP_* vars if !strings.HasPrefix(envName, envAppPrefix) { continue } // Retrieve app name nextSep := strings.Index(envName[envAppPrefixLen:], "_") if nextSep < 0 { nextSep = 0 } name := envName[envAppPrefixLen : envAppPrefixLen+nextSep] appName := strings.ToLower(name) // Skip if we processed this app already if _, ok := apps[appName]; ok { continue } // Retrieve info for app var pubkey *rsa.PublicKey = nil if key, ok := os.LookupEnv(envAppPrefix + name + "_PEM"); ok { key, _ := pem.Decode([]byte(key)) pub, err := x509.ParsePKCS1PublicKey(key.Bytes) if err != nil { log.Fatalf("Error while parsing PEM/PKCS1 block for %s: %s", envAppPrefix+name+"_PEM", err.Error()) } pubkey = pub } apps[appName] = appDef{ Name: mustGetEnv(envAppPrefix + name + "_NAME"), WebhookURL: mustGetEnv(envAppPrefix + name + "_URL"), PublicKey: pubkey, } log.Printf("app registered: %s", appName) } http.HandleFunc("/.well-known/host-meta", func(w http.ResponseWriter, r *http.Request) { headers := w.Header() headers.Add("Content-Type", "application/xrd+xml") headers.Add("Content-Type", "charset=utf-8") fmt.Fprintf(w, ``, host) }) http.HandleFunc("/.well-known/webfinger", func(w http.ResponseWriter, r *http.Request) { params := r.URL.Query() resource := params.Get("resource") if resource == "" { http.Error(w, "must include a valid resource", http.StatusBadRequest) return } parts := strings.SplitN(resource, ":", 2) if len(parts) < 2 { http.Error(w, "invalid resource specified", http.StatusBadRequest) return } switch parts[0] { case "acct": // Split user by host userParts := strings.SplitN(parts[1], "@", 2) if len(userParts) < 2 || strings.ToLower(userParts[1]) != host { http.Error(w, "Invalid user specified", http.StatusBadRequest) return } // Check if user is a registered app _, ok := apps[userParts[0]] if !ok { http.Error(w, "user not found", http.StatusNotFound) return } headers := w.Header() headers.Add("Content-Type", "application/json") headers.Add("Content-Type", "charset=utf-8") err := json.NewEncoder(w).Encode(profileWebFingerData{ Subject: resource, Links: []profileLink{ { Rel: "self", Type: "application/activity+json", Href: fmt.Sprintf("%s/u/%s", fullhost, userParts[0]), }, }, }) if err != nil { http.Error(w, "Error encoding webfinger: "+err.Error(), http.StatusInternalServerError) } } }) http.HandleFunc("/u/", func(w http.ResponseWriter, r *http.Request) { parts := strings.SplitN(strings.Trim(r.URL.Path, " /"), "/", 2) if len(parts) < 2 { http.Error(w, "page not found", http.StatusNotFound) return } name := parts[1] app, ok := apps[name] if !ok { http.Error(w, "page not found", http.StatusNotFound) return } userURL := fmt.Sprintf("%s/u/%s", fullhost, name) contextes := []string{ "https://www.w3.org/ns/activitystreams", } var pubkey *pubKeyBlock = nil if app.PublicKey != nil { pubKeyBytes := x509.MarshalPKCS1PublicKey(app.PublicKey) pubKeyPem := string(pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: pubKeyBytes, })) contextes = append(contextes, "https://w3id.org/security/v1") pubkey = &pubKeyBlock{ ID: userURL + "#main-key", Owner: userURL, PEM: pubKeyPem, } } headers := w.Header() headers.Add("Content-Type", "application/activity+json") headers.Add("Content-Type", "charset=utf-8") err := json.NewEncoder(w).Encode(profilePage{ Context: contextes, ID: userURL, Type: "Person", PreferredUsername: name, Name: app.Name, Inbox: fmt.Sprintf("%s/inbox", fullhost), Followers: fmt.Sprintf("%s/followers", userURL), PublicKey: pubkey, }) if err != nil { http.Error(w, "Error encoding page: "+err.Error(), http.StatusInternalServerError) } }) http.HandleFunc("/inbox", func(w http.ResponseWriter, r *http.Request) { byt, _ := ioutil.ReadAll(r.Body) fmt.Println(string(byt)) }) if err := http.ListenAndServe(bind, nil); err != nil { log.Fatalf("error while listening: %f", err) } }