underbbs/server/api.go

309 lines
8.4 KiB
Go

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\" }}<html><body><h1>ERROR {{ $params.ErrorCode }}</h1><p class='error'>{{ $params.ErrorMessage }}</p></body></html>")
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<adapter_id>\S+)/subscribe`, ProtectWithSubscriberKey(
apiAdapterSubscribe(renderer.JSON("data"), self.subscribers),
self.subscribers,
))
rtr.Get(`/adapters/(?P<adapter_id>\S+)/fetch`, ProtectWithSubscriberKey(
apiAdapterFetch(renderer.JSON("data"), self.subscribers),
self.subscribers,
))
rtr.Post(`/adapters/(?P<adapter_id>\S+)/do`, ProtectWithSubscriberKey(
apiAdapterDo(renderer.JSON("data"), self.subscribers),
self.subscribers,
))
if self.apKey != nil && self.apDomain != nil {
rtr.Get("/actor", apiGetActor(renderer.JSON("actor"), *self.apKey, *self.apDomain))
}
return http.HandlerFunc(rtr.ServeHTTP)
}