commit d6b0c343f75eded7e903ef562745ce7508fe0345 Author: Hamcha Date: Thu Feb 25 15:01:43 2021 +0100 Check in diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3156992 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.exe +_test \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f929585 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.fromouter.space/Hamcha/Aptweb + +go 1.15 diff --git a/main.go b/main.go new file mode 100644 index 0000000..cb88e9c --- /dev/null +++ b/main.go @@ -0,0 +1,230 @@ +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) + } +}