package server import ( "bytes" "crypto" "crypto/x509" "encoding/base64" "encoding/json" "errors" "fmt" "forge.lightcrystal.systems/nilix/quartzgun/renderer" "forge.lightcrystal.systems/nilix/quartzgun/router" "forge.lightcrystal.systems/nilix/quartzgun/util" "forge.lightcrystal.systems/nilix/underbbs/adapter" "forge.lightcrystal.systems/nilix/underbbs/models" "html/template" "log" "net/http" "strings" ) type PrivKeyAux interface { Public() crypto.PublicKey Equal(x crypto.PrivateKey) bool } type doRequest struct { Action string `json:"action"` Content *string `json:"content,omitempty"` File *string `json:"file,omitempty"` Desc *string `json:"desc,omitempty"` } func getSubscriberKey(req *http.Request) string { return req.Header.Get("X-Underbbs-Subscriber") } func getSubscriberByKey(key string, subscribers map[*Subscriber][]adapter.Adapter) *Subscriber { for s, _ := range subscribers { if s.key == key { return s } } return nil } func setAdaptersForSubscriber(key string, adapters []adapter.Adapter, subscribers map[*Subscriber][]adapter.Adapter) error { var ptr *Subscriber = nil for s, _ := range subscribers { if s.key == key { ptr = s } } if ptr != nil { subscribers[ptr] = adapters return nil } return errors.New("subscriber not present in map") } func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter, apKey *crypto.PrivateKey) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { skey := getSubscriberKey(req) subscriber := getSubscriberByKey(skey, subscribers) if subscriber == nil { w.WriteHeader(404) return } // decode adapter config from request body settings := make([]models.Settings, 0) err := json.NewDecoder(req.Body).Decode(&settings) if err != nil { w.WriteHeader(400) next.ServeHTTP(w, req) return } // iterate through settings and create adapters adapters := make([]adapter.Adapter, 0) for _, s := range settings { var a adapter.Adapter switch s.Protocol { case "nostr": a = &adapter.NostrAdapter{} case "mastodon": a = &adapter.MastoAdapter{} s.ApSigner = apKey case "misskey": a = &adapter.MisskeyAdapter{} s.ApSigner = apKey case "honk": a = &adapter.HonkAdapter{} s.ApSigner = apKey default: break } err := a.Init(s, &subscriber.data) if err != nil { util.AddContextValue(req, "data", err.Error()) w.WriteHeader(500) next.ServeHTTP(w, req) return } log.Print("adapter initialized; adding to array") adapters = append(adapters, a) log.Print("adapter added to array") } // TODO: cancel subscriptions on any existing adapters // store the adapters in the subscriber map err = setAdaptersForSubscriber(skey, adapters, subscribers) if err != nil { util.AddContextValue(req, "data", err.Error()) w.WriteHeader(500) next.ServeHTTP(w, req) } w.WriteHeader(201) next.ServeHTTP(w, req) }) } type subscribeParams struct { Filter string `json:"filter"` Target *string `json:"target,omitempty"` } func apiAdapterSubscribe(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { skey := getSubscriberKey(req) subscriber := getSubscriberByKey(skey, subscribers) if subscriber == nil { w.WriteHeader(404) return } adapters := subscribers[subscriber] urlParams := req.Context().Value("params").(map[string]string) adapter := urlParams["adapter_id"] sp := subscribeParams{} err := json.NewDecoder(req.Body).Decode(&sp) if err != nil { w.WriteHeader(500) fmt.Println(err.Error()) } for _, a := range adapters { if a.Name() == adapter { fmt.Printf("adapter.subscribe call: %s {%s, %s}\n", adapter, sp.Filter, *sp.Target) a.Subscribe(sp.Filter, sp.Target) w.WriteHeader(201) next.ServeHTTP(w, req) } } w.WriteHeader(404) }) } func ProtectWithSubscriberKey(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { skey := getSubscriberKey(req) if getSubscriberByKey(skey, subscribers) != nil { next.ServeHTTP(w, req) return } w.WriteHeader(http.StatusUnauthorized) }) } func apiAdapterFetch(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { skey := getSubscriberKey(req) s := getSubscriberByKey(skey, subscribers) if s != nil { apiParams := req.Context().Value("params").(map[string]string) queryParams := req.URL.Query() for _, a := range subscribers[s] { if a.Name() == apiParams["adapter_id"] { err := a.Fetch(queryParams["entity_type"][0], queryParams["entity_id"]) if err != nil { log.Print(err.Error()) w.WriteHeader(http.StatusInternalServerError) } else { w.WriteHeader(http.StatusAccepted) } next.ServeHTTP(w, req) return } } } }) } func apiAdapterDo(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { skey := getSubscriberKey(req) s := getSubscriberByKey(skey, subscribers) if s != nil { apiParams := req.Context().Value("params").(map[string]string) for _, a := range subscribers[s] { if a.Name() == apiParams["adapter_id"] { // request body is json // if we have a `file`, it needs to be transformed from base64 to standard bytes/string doReq := map[string]string{} err := json.NewDecoder(req.Body).Decode(&doReq) if err != nil { w.WriteHeader(422) return } if f, exists := doReq["file"]; exists { firstCommaIdx := strings.Index(f, ",") rawFile, err := base64.StdEncoding.DecodeString(f[firstCommaIdx+1:]) if err != nil { w.WriteHeader(500) return } doReq["file"] = string(rawFile) } action, ok := doReq["action"] if ok { delete(doReq, "action") } else { w.WriteHeader(422) return } a.Do(action, doReq) next.ServeHTTP(w, req) return } } } }) } func apiGetActor(next http.Handler, apKey crypto.PrivateKey, apDomain string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { pubkey := apKey.(PrivKeyAux).Public() pubkeypem, err := x509.MarshalPKIXPublicKey(pubkey) if err != nil { w.WriteHeader(500) return } encodedkey := make([]byte, len(pubkeypem)*2) base64.StdEncoding.Encode(encodedkey, pubkeypem) lastpos := bytes.Index(encodedkey, []byte{byte(0)}) keystring := "-----BEGIN PUBLIC KEY-----\n" keystring += string(encodedkey[:lastpos]) keystring += "\n-----END PUBLIC KEY-----\n" actor := adapter.ApActor{ Id: apDomain + "/api/actor", Name: "underbbs_actor", PreferredUsername: "underbbs_actor", Summary: "gateway for building modular social integrations", Url: apDomain + "/api/actor", PublicKey: adapter.ApKey{ Id: apDomain + "/api/actor#key", Owner: apDomain + "/api/actor", PublicKeyPem: keystring, }, } util.AddContextValue(req, "actor", actor) next.ServeHTTP(w, req) }) } func (self *BBSServer) apiMux() http.Handler { errTemplate, err := template.New("err").Parse("{{ $params := (.Context).Value \"params\" }}
{{ $params.ErrorMessage }}
") if err != nil { panic("error template was malformed") } rtr := &router.Router{ Fallback: *errTemplate, } rtr.Post("/adapters", ProtectWithSubscriberKey( apiConfigureAdapters(renderer.JSON("data"), self.subscribers, self.apKey), self.subscribers, )) rtr.Post(`/adapters/(?P