diff --git a/adapter/anonAp.go b/adapter/anonAp.go index 5da6eaa..98e2b45 100644 --- a/adapter/anonAp.go +++ b/adapter/anonAp.go @@ -1,6 +1,7 @@ package adapter import ( + "crypto" "encoding/json" "fmt" "io" @@ -10,7 +11,7 @@ import ( "time" "forge.lightcrystal.systems/nilix/underbbs/models" - _ "github.com/go-fed/httpsig" + "github.com/go-fed/httpsig" ) type anonAPAdapter struct { @@ -19,6 +20,8 @@ type anonAPAdapter struct { client http.Client protocol string nickname string + actorKey *crypto.PrivateKey + actorURL *string } type apLink struct { @@ -47,15 +50,25 @@ type apIcon struct { Url string } +type apKey struct { + Id string + Owner string + PublicKeyPem string +} + type apActor struct { - Icon apIcon + Icon *apIcon `json:"icon",omitempty` Id string Name string PreferredUsername string Summary string Url string + PublicKey ApKey } +type ApActor apActor +type ApKey apKey + type apOutbox struct { OrderedItems []interface{} `json:"-"` RawOrderedItems []json.RawMessage `json:"OrderedItems"` @@ -126,14 +139,28 @@ type webFinger struct { Links []apLink } -func (self *anonAPAdapter) Init(data *chan models.SocketData, server, protocol, nickname string) error { +func (self *anonAPAdapter) Init(data *chan models.SocketData, server, protocol, nickname string, apSigner *crypto.PrivateKey, apActorURL *string) error { self.data = data self.server = server self.nickname = nickname self.protocol = protocol + self.actorKey = apSigner + self.actorURL = apActorURL return nil } +func (self *anonAPAdapter) signRequest(req *http.Request) error { + prefs := []httpsig.Algorithm{httpsig.RSA_SHA512, httpsig.ED25519} + // The "Date" and "Digest" headers must already be set on r, as well as r.URL. + headersToSign := []string{httpsig.RequestTarget} + signer, _, err := httpsig.NewSigner(prefs, httpsig.DigestSha512, headersToSign, httpsig.Signature, 0) + if err != nil { + return err + } + + return signer.SignRequest(self.actorKey, *self.actorURL+"/api/actor", req, nil) +} + func getBodyJson(res *http.Response) []byte { l := res.ContentLength // if the length is unknown, we'll allocate 4k @@ -154,6 +181,10 @@ func (self *anonAPAdapter) makeApRequest(method, url string, data io.Reader) (*h } req.Header.Set("Accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + + if self.actorKey != nil && self.actorURL != nil { + self.signRequest(req) + } return self.client.Do(req) } @@ -220,27 +251,27 @@ func (self *anonAPAdapter) toAuthor(actor apActor) *models.Author { } func (self *anonAPAdapter) getHostForId(id string) string { - if strings.HasPrefix(id, "https://") { - idNoScheme := strings.Split(id, "https://")[1] - serverNoScheme := strings.Split(idNoScheme, "/")[0] - return "https://" + serverNoScheme - } else { - return "https://" + strings.Split(id, "@")[1] - } + if strings.HasPrefix(id, "https://") { + idNoScheme := strings.Split(id, "https://")[1] + serverNoScheme := strings.Split(idNoScheme, "/")[0] + return "https://" + serverNoScheme + } else { + return "https://" + strings.Split(id, "@")[1] + } } func (self *anonAPAdapter) normalizeActorId(id string) string { - if string([]byte{id[0]}) == "@" { - id = id[1:] - } - if !strings.Contains(id, "@") { - // if the id is not a URI, add local server to it - if !strings.HasPrefix(id, "https://") { - serverNoScheme := strings.Split(self.server, "https://")[1] - id = fmt.Sprintf("%s@%s", id, serverNoScheme) - } - } - return id + if string([]byte{id[0]}) == "@" { + id = id[1:] + } + if !strings.Contains(id, "@") { + // if the id is not a URI, add local server to it + if !strings.HasPrefix(id, "https://") { + serverNoScheme := strings.Split(self.server, "https://")[1] + id = fmt.Sprintf("%s@%s", id, serverNoScheme) + } + } + return id } func (self *anonAPAdapter) Fetch(etype string, ids []string) error { @@ -253,21 +284,21 @@ func (self *anonAPAdapter) Fetch(etype string, ids []string) error { profile := normalizedId fmt.Println(reqHost) if !strings.HasPrefix(normalizedId, "https://") { - res, err := http.Get(reqHost + "/.well-known/webfinger?resource=acct:" + normalizedId) - if err != nil { - return err - } - data := getBodyJson(res) - wf := webFinger{} - json.Unmarshal(data, &wf) + res, err := http.Get(reqHost + "/.well-known/webfinger?resource=acct:" + normalizedId) + if err != nil { + return err + } + data := getBodyJson(res) + wf := webFinger{} + json.Unmarshal(data, &wf) - for _, l := range wf.Links { - if l.Rel == "self" { - profile = l.Href - break + for _, l := range wf.Links { + if l.Rel == "self" { + profile = l.Href + break + } } } - } res, err := self.makeApRequest("GET", profile, nil) if err != nil { return err @@ -284,19 +315,19 @@ func (self *anonAPAdapter) Fetch(etype string, ids []string) error { reqHost := self.getHostForId(normalizedId) profile := normalizedId if !strings.HasPrefix(normalizedId, "https://") { - res, err := http.Get(reqHost + "/.well-known/webfinger?resource=acct:" + normalizedId) - if err != nil { - return err - } - data := getBodyJson(res) - wf := webFinger{} - json.Unmarshal(data, &wf) - for _, l := range wf.Links { - if l.Rel == "self" { - profile = l.Href - break + res, err := http.Get(reqHost + "/.well-known/webfinger?resource=acct:" + normalizedId) + if err != nil { + return err + } + data := getBodyJson(res) + wf := webFinger{} + json.Unmarshal(data, &wf) + for _, l := range wf.Links { + if l.Rel == "self" { + profile = l.Href + break + } } - } } res, err := self.makeApRequest("GET", profile+"/outbox", nil) if err != nil { diff --git a/adapter/honk.go b/adapter/honk.go index d41476e..5af66fc 100644 --- a/adapter/honk.go +++ b/adapter/honk.go @@ -2,6 +2,7 @@ package adapter import ( "bytes" + "crypto" "encoding/json" "errors" "fmt" @@ -49,6 +50,8 @@ type HonkAdapter struct { username string password string token string + apSigner *crypto.PrivateKey + apDomain *string cache map[string]time.Time maxId int @@ -80,6 +83,7 @@ func (self *HonkAdapter) Init(settings Settings, data *chan SocketData) error { self.username = parts[1] self.server = "https://" + parts[2] self.nickname = settings.Nickname + self.apSigner = settings.ApSigner if settings.Password == nil || *settings.Password == "" { // we're anonymous! @@ -225,7 +229,7 @@ func (self *HonkAdapter) toMsg(h honk, target *string) Message { func (self *HonkAdapter) Fetch(etype string, ids []string) error { // honk API is limited, we fall back to the anonymous adapter for fetch ops aaa := anonAPAdapter{} - aaa.Init(self.data, self.server, "honk", self.nickname) + aaa.Init(self.data, self.server, "honk", self.nickname, self.apSigner, self.apDomain) return aaa.Fetch(etype, ids) } diff --git a/cli/cli.go b/cli/cli.go index f7754ad..397459f 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,71 +1,21 @@ package cli import ( - "encoding/json" "errors" "io/ioutil" "log" - "os" - "path/filepath" - "runtime" "strings" "forge.lightcrystal.systems/nilix/underbbs/adapter" "forge.lightcrystal.systems/nilix/underbbs/models" ) -func GetConfigLocation() string { - home := os.Getenv("HOME") - appdata := os.Getenv("APPDATA") - switch runtime.GOOS { - case "windows": - return filepath.Join(appdata, "underbbs") - case "darwin": - return filepath.Join(home, "Library", "Application Support", "underbbs") - case "plan9": - return filepath.Join(home, "lib", "underbbs") - default: - return filepath.Join(home, ".config", "underbbs") - } -} - -func EnsureConfigLocationExists() { - fileInfo, err := os.Stat(GetConfigLocation()) - - if os.IsNotExist(err) { - os.MkdirAll(GetConfigLocation(), os.ModePerm) - } else if !fileInfo.IsDir() { - panic("Config location is not a directory!") - } -} - -func Process(args ...string) error { - // allocate storage for the settings array - var settings []models.Settings - var s *models.Settings - - if len(args) < 3 { - return errors.New("CLI requires at least 3 args: ADAPTER ACTION DATA...") - } - - EnsureConfigLocationExists() - cfgdir := GetConfigLocation() - - // get adapter from first arg +func Process(gsettings models.GlobalSettings, args ...string) error { adapterName := args[0] args = args[1:] + var s *models.Settings - // get config from config fle based on adapter - content, err := ioutil.ReadFile(filepath.Join(cfgdir, "config.json")) - if err != nil { - return err - } - - err = json.Unmarshal(content, &settings) - if err != nil { - return err - } - for _, x := range settings { + for _, x := range gsettings.Adapters { if x.Nickname == adapterName { s = &x break diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..a56db17 --- /dev/null +++ b/config/config.go @@ -0,0 +1,55 @@ +package config + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + "runtime" + + "forge.lightcrystal.systems/nilix/underbbs/models" +) + +func getConfigLocation() string { + home := os.Getenv("HOME") + appdata := os.Getenv("APPDATA") + switch runtime.GOOS { + case "windows": + return filepath.Join(appdata, "underbbs") + case "darwin": + return filepath.Join(home, "Library", "Application Support", "underbbs") + case "plan9": + return filepath.Join(home, "lib", "underbbs") + default: + return filepath.Join(home, ".config", "underbbs") + } +} + +func ensureConfigLocationExists() { + fileInfo, err := os.Stat(getConfigLocation()) + + if os.IsNotExist(err) { + os.MkdirAll(getConfigLocation(), os.ModePerm) + } else if !fileInfo.IsDir() { + panic("Config location is not a directory!") + } +} + +func LoadConfig() (models.GlobalSettings, error) { + var settings models.GlobalSettings + + ensureConfigLocationExists() + cfgdir := getConfigLocation() + + // get config from config fle based on adapter + content, err := ioutil.ReadFile(filepath.Join(cfgdir, "config.json")) + if err != nil { + return settings, err + } + + err = json.Unmarshal(content, &settings) + if err != nil { + return settings, err + } + return settings, nil +} diff --git a/frontend/ts/util.ts b/frontend/ts/util.ts index 365a9d5..799c539 100644 --- a/frontend/ts/util.ts +++ b/frontend/ts/util.ts @@ -30,7 +30,7 @@ function closeErr(): void { async function authorizedFetch(method: string, uri: string, body: any): Promise { const headers = new Headers() - headers.set('Authorization', 'Bearer ' + DatagramSocket.skey) + headers.set('X-Underbbs-Subscriber', DatagramSocket.skey ?? "") return await fetch(uri, { method: method, headers: headers, diff --git a/frontend/ts/websocket.ts b/frontend/ts/websocket.ts index 0ff1ced..25bc902 100644 --- a/frontend/ts/websocket.ts +++ b/frontend/ts/websocket.ts @@ -30,7 +30,7 @@ export class DatagramSocket { console.log(data); if (data.key) { DatagramSocket.skey = data.key; - util.authorizedFetch("POST", DatagramSocket.gatewayWithScheme() + "/api/adapters", JSON.stringify(Settings._instance.adapters)) + util.authorizedFetch("POST", DatagramSocket._gateway + "/api/adapters", JSON.stringify(Settings._instance.adapters)) .then(r=> { if (r.ok) { // iterate through any components which might want to fetch data @@ -96,13 +96,17 @@ export class DatagramSocket { } static connect(gateway: string): void { + DatagramSocket._gateway = gateway; - - const wsProto = location.protocol == "https:" ? "wss" : "ws"; + + const wsProto = gateway.startsWith("https://") ? "wss" : "ws"; let wsUrl = "" if (!gateway) { wsUrl = `${wsProto}://${location.host}/subscribe`; } else { + if (gateway.startsWith("https:")) { + gateway = gateway.split("https://")[1]; + } wsUrl = wsProto + "://" + gateway + "/subscribe" } const _conn = new WebSocket(wsUrl, "underbbs"); diff --git a/go.mod b/go.mod index 4f3fff4..963fb4f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/McKael/madon v2.3.0+incompatible github.com/go-fed/httpsig v1.1.0 github.com/nbd-wtf/go-nostr v0.31.2 + github.com/rs/cors v1.11.1 github.com/yitsushi/go-misskey v1.1.6 golang.org/x/time v0.5.0 nhooyr.io/websocket v1.8.11 diff --git a/go.sum b/go.sum index aedca46..4debdcf 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= diff --git a/models/settings.go b/models/settings.go index e4f6a90..76a3c16 100644 --- a/models/settings.go +++ b/models/settings.go @@ -1,12 +1,24 @@ package models +import ( + "crypto" +) + +type GlobalSettings struct { + ApKey *string `json:"apkey",omitempty` + ApDomain *string `json:"apDomain",omitempty` + Port int `json:"port"` + Adapters []Settings `json:"adapters"` +} + type Settings struct { Nickname string Protocol string - PrivKey *string `json:"privkey",omitempty` - Relays []string `json:"relays",omitempty` - Server *string `json:"server",omitempty` - ApiKey *string `json:"apiKey",omitempty` - Handle *string `json:"handle",omitempty` - Password *string `json:"password",omitempty` + PrivKey *string `json:"privkey",omitempty` + Relays []string `json:"relays",omitempty` + Server *string `json:"server",omitempty` + ApiKey *string `json:"apiKey",omitempty` + Handle *string `json:"handle",omitempty` + Password *string `json:"password",omitempty` + ApSigner *crypto.PrivateKey `json:"_",omitempty` } diff --git a/server/api.go b/server/api.go index c54a467..d8cc68d 100644 --- a/server/api.go +++ b/server/api.go @@ -1,6 +1,9 @@ package server import ( + "bytes" + "crypto" + "crypto/x509" "encoding/base64" "encoding/json" "errors" @@ -16,6 +19,11 @@ import ( "strings" ) +type PrivKeyAux interface { + Public() crypto.PublicKey + Equal(x crypto.PrivateKey) bool +} + type doRequest struct { Action string `json:"action"` Content *string `json:"content,omitempty"` @@ -23,12 +31,8 @@ type doRequest struct { Desc *string `json:"desc,omitempty"` } -func getSubscriberKey(req *http.Request) (string, error) { - authHeader := req.Header.Get("Authorization") - if strings.HasPrefix(authHeader, "Bearer ") { - return strings.Split(authHeader, "Bearer ")[1], nil - } - return "", errors.New("No subscriber key") +func getSubscriberKey(req *http.Request) string { + return req.Header.Get("X-Underbbs-Subscriber") } func getSubscriberByKey(key string, subscribers map[*Subscriber][]adapter.Adapter) *Subscriber { @@ -42,36 +46,30 @@ func getSubscriberByKey(key string, subscribers map[*Subscriber][]adapter.Adapte func setAdaptersForSubscriber(key string, adapters []adapter.Adapter, subscribers map[*Subscriber][]adapter.Adapter) error { var ptr *Subscriber = nil - log.Print("looking for subscriber in map...") for s, _ := range subscribers { if s.key == key { ptr = s } } if ptr != nil { - log.Print("setting adaters for the found subscriber: " + ptr.key) subscribers[ptr] = adapters return nil } return errors.New("subscriber not present in map") } -func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { +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) { - // get subscriber key - skey, err := getSubscriberKey(req) - if err != nil { - w.WriteHeader(500) - return - } + 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) + err := json.NewDecoder(req.Body).Decode(&settings) if err != nil { w.WriteHeader(400) next.ServeHTTP(w, req) @@ -86,10 +84,13 @@ func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapt 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 @@ -129,12 +130,7 @@ type subscribeParams struct { func apiAdapterSubscribe(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - // get subscriber key - skey, err := getSubscriberKey(req) - if err != nil { - w.WriteHeader(500) - return - } + skey := getSubscriberKey(req) subscriber := getSubscriberByKey(skey, subscribers) if subscriber == nil { w.WriteHeader(404) @@ -146,7 +142,11 @@ func apiAdapterSubscribe(next http.Handler, subscribers map[*Subscriber][]adapte urlParams := req.Context().Value("params").(map[string]string) adapter := urlParams["adapter_id"] sp := subscribeParams{} - err = json.NewDecoder(req.Body).Decode(&sp) + 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) @@ -158,19 +158,15 @@ func apiAdapterSubscribe(next http.Handler, subscribers map[*Subscriber][]adapte } } w.WriteHeader(404) - next.ServeHTTP(w, req) }) } func ProtectWithSubscriberKey(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - authHeader := req.Header.Get("Authorization") - if strings.HasPrefix(authHeader, "Bearer ") { - subscriberKey := strings.Split(authHeader, "Bearer ")[1] - if getSubscriberByKey(subscriberKey, subscribers) != nil { - next.ServeHTTP(w, req) - return - } + skey := getSubscriberKey(req) + if getSubscriberByKey(skey, subscribers) != nil { + next.ServeHTTP(w, req) + return } w.WriteHeader(http.StatusUnauthorized) }) @@ -178,74 +174,101 @@ func ProtectWithSubscriberKey(next http.Handler, subscribers map[*Subscriber][]a func apiAdapterFetch(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - authHeader := req.Header.Get("Authorization") - if strings.HasPrefix(authHeader, "Bearer ") { - subscriberKey := strings.Split(authHeader, "Bearer ")[1] - s := getSubscriberByKey(subscriberKey, 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 + 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 } } } - w.WriteHeader(http.StatusUnauthorized) }) } func apiAdapterDo(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - authHeader := req.Header.Get("Authorization") - if strings.HasPrefix(authHeader, "Bearer ") { - subscriberKey := strings.Split(authHeader, "Bearer ")[1] - s := getSubscriberByKey(subscriberKey, 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) + 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 } } } - w.WriteHeader(http.StatusUnauthorized) + }) +} + +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) }) } @@ -259,7 +282,7 @@ func (self *BBSServer) apiMux() http.Handler { } rtr.Post("/adapters", ProtectWithSubscriberKey( - apiConfigureAdapters(renderer.JSON("data"), self.subscribers), + apiConfigureAdapters(renderer.JSON("data"), self.subscribers, self.apKey), self.subscribers, )) @@ -278,5 +301,9 @@ func (self *BBSServer) apiMux() http.Handler { 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) } diff --git a/server/server.go b/server/server.go index ad76344..32fe7e4 100644 --- a/server/server.go +++ b/server/server.go @@ -2,11 +2,15 @@ package server import ( "context" + "crypto" + "crypto/x509" + "encoding/pem" "errors" "forge.lightcrystal.systems/nilix/quartzgun/cookie" "forge.lightcrystal.systems/nilix/quartzgun/renderer" "forge.lightcrystal.systems/nilix/underbbs/adapter" "forge.lightcrystal.systems/nilix/underbbs/models" + _ "github.com/rs/cors" "golang.org/x/time/rate" "io/ioutil" "log" @@ -30,20 +34,70 @@ type BBSServer struct { serveMux http.ServeMux subscribersLock sync.Mutex subscribers map[*Subscriber][]adapter.Adapter + apKey *crypto.PrivateKey + apDomain *string } -func New() *BBSServer { - srvr := &BBSServer{ - subscribeMessageBuffer: 16, - logf: log.Printf, - subscribers: make(map[*Subscriber][]adapter.Adapter), +func CORS(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Credentials", "true") + w.Header().Add("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-Underbbs-Subscriber") + w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + + if r.Method == "OPTIONS" { + http.Error(w, "No Content", http.StatusNoContent) + return + } + + next.ServeHTTP(w, r) + }) +} + +func New(config models.GlobalSettings) *BBSServer { + var apKey crypto.PrivateKey + + useApKey := false + + if config.ApKey != nil && config.ApDomain != nil { + keybytes, err := ioutil.ReadFile(*config.ApKey) + if err != nil { + panic(err.Error()) + } + keypem, _ := pem.Decode(keybytes) + if keypem == nil { + panic("couldn't decode pem block from AP key") + } + apKey, err = x509.ParsePKCS8PrivateKey(keypem.Bytes) + if err != nil { + panic(err.Error()) + } + useApKey = true + } + + var srvr *BBSServer + + if useApKey { + srvr = &BBSServer{ + subscribeMessageBuffer: 16, + logf: log.Printf, + subscribers: make(map[*Subscriber][]adapter.Adapter), + apKey: &apKey, + apDomain: config.ApDomain, + } + } else { + srvr = &BBSServer{ + subscribeMessageBuffer: 16, + logf: log.Printf, + subscribers: make(map[*Subscriber][]adapter.Adapter), + } } // frontend is here srvr.serveMux.Handle("/app/", http.StripPrefix("/app/", renderer.Subtree("./frontend/dist"))) // api - srvr.serveMux.Handle("/api/", http.StripPrefix("/api", srvr.apiMux())) + srvr.serveMux.Handle("/api/", http.StripPrefix("/api", CORS(srvr.apiMux()))) // websocket srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler) diff --git a/underbbs.go b/underbbs.go index b55b2ef..0aac919 100644 --- a/underbbs.go +++ b/underbbs.go @@ -13,6 +13,8 @@ import ( "time" "forge.lightcrystal.systems/nilix/underbbs/cli" + "forge.lightcrystal.systems/nilix/underbbs/config" + "forge.lightcrystal.systems/nilix/underbbs/models" "forge.lightcrystal.systems/nilix/underbbs/server" ) @@ -23,11 +25,18 @@ func main() { var err error = nil progname := filepath.Base(args[0]) + + cfg, err := config.LoadConfig() + + if err != nil { + panic(err.Error()) + } + switch progname { case "underbbs-cli": - err = run_cli(args[1:]...) + err = run_cli(cfg, args[1:]...) default: - err = run_srvr() + err = run_srvr(cfg) } if err != nil { @@ -35,17 +44,17 @@ func main() { } } -func run_cli(args ...string) error { - return cli.Process(args...) +func run_cli(settings models.GlobalSettings, args ...string) error { + return cli.Process(settings, args...) } -func run_srvr() error { - l, err := net.Listen("tcp", ":"+strconv.FormatInt(int64(9090), 10)) +func run_srvr(settings models.GlobalSettings) error { + l, err := net.Listen("tcp", ":"+strconv.FormatInt(int64(settings.Port), 10)) if err != nil { return err } - bbsServer := server.New() + bbsServer := server.New(settings) s := &http.Server{ Handler: bbsServer, ReadTimeout: time.Second * 10,