implement adapter configuration API

This commit is contained in:
Iris Lightshard 2024-05-26 17:24:49 -06:00
parent 8f2b281d82
commit 63b79409f4
Signed by: Iris Lightshard
GPG key ID: 688407174966CAF3
2 changed files with 125 additions and 22 deletions

View file

@ -1,14 +1,101 @@
package server package server
import ( import (
"encoding/json"
"errors"
"forge.lightcrystal.systems/lightcrystal/underbbs/adapter"
"forge.lightcrystal.systems/lightcrystal/underbbs/models"
"hacklab.nilfm.cc/quartzgun/renderer" "hacklab.nilfm.cc/quartzgun/renderer"
"hacklab.nilfm.cc/quartzgun/router" "hacklab.nilfm.cc/quartzgun/router"
"hacklab.nilfm.cc/quartzgun/util"
"html/template" "html/template"
"net/http" "net/http"
"strings"
) )
func apiConfigureAdapters(next http.Handler) http.Handler { 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 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) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// get subscriber key
skey, err := getSubscriberKey(req)
if err != nil {
w.WriteHeader(500)
return
}
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{}
break
default:
break
}
err := a.Init(s, subscriber.data)
if err != nil {
util.AddContextValue(req, "data", err.Error())
w.WriteHeader(500)
next.ServeHTTP(w, req)
}
adapters = append(adapters, a)
}
// 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) w.WriteHeader(201)
next.ServeHTTP(w, req) next.ServeHTTP(w, req)
}) })
@ -28,14 +115,23 @@ func apiAdapterSubscribe(next http.Handler) http.Handler {
}) })
} }
func ProtectWithSubscriberKey(next http.Handler) http.Handler { func ProtectWithSubscriberKey(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(201) authHeader := req.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
subscriberKey := strings.Split(authHeader, "Bearer ")[1]
for s, _ := range subscribers {
if s.key == subscriberKey {
next.ServeHTTP(w, req) next.ServeHTTP(w, req)
return
}
}
}
w.WriteHeader(http.StatusUnauthorized)
}) })
} }
func apiMux() http.Handler { 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>") 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 { if err != nil {
panic("error template was malformed") panic("error template was malformed")
@ -45,10 +141,20 @@ func apiMux() http.Handler {
} }
// adapters (POST & GET) // adapters (POST & GET)
rtr.Post("/adapters", ProtectWithSubscriberKey(apiConfigureAdapters(renderer.JSON("data")))) rtr.Post("/adapters", ProtectWithSubscriberKey(
rtr.Get("/adapters", ProtectWithSubscriberKey(apiGetAdapters(renderer.JSON("data")))) apiConfigureAdapters(renderer.JSON("data"), self.subscribers),
self.subscribers,
))
rtr.Get("/adapters", ProtectWithSubscriberKey(
apiGetAdapters(renderer.JSON("data")),
self.subscribers,
))
// adapters/:name/subscribe // adapters/:name/subscribe
rtr.Post(`/adapters/(?P<id>\S+)/subscribe`, ProtectWithSubscriberKey(apiAdapterSubscribe(renderer.JSON("data")))) rtr.Post(`/adapters/(?P<id>\S+)/subscribe`, ProtectWithSubscriberKey(
apiAdapterSubscribe(renderer.JSON("data")),
self.subscribers,
))
return http.HandlerFunc(rtr.ServeHTTP) return http.HandlerFunc(rtr.ServeHTTP)
} }

View file

@ -3,7 +3,6 @@ package server
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"forge.lightcrystal.systems/lightcrystal/underbbs/adapter" "forge.lightcrystal.systems/lightcrystal/underbbs/adapter"
"forge.lightcrystal.systems/lightcrystal/underbbs/models" "forge.lightcrystal.systems/lightcrystal/underbbs/models"
"golang.org/x/time/rate" "golang.org/x/time/rate"
@ -20,6 +19,7 @@ import (
type Subscriber struct { type Subscriber struct {
key string key string
msgs chan []byte msgs chan []byte
data chan models.SocketData
closeSlow func() closeSlow func()
} }
@ -43,7 +43,7 @@ func New() *BBSServer {
srvr.serveMux.Handle("/app/", http.StripPrefix("/app/", renderer.Subtree("./dist"))) srvr.serveMux.Handle("/app/", http.StripPrefix("/app/", renderer.Subtree("./dist")))
// api // api
srvr.serveMux.Handle("/api/", http.StripPrefix("/api", apiMux())) srvr.serveMux.Handle("/api/", http.StripPrefix("/api", srvr.apiMux()))
// websocket // websocket
srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler) srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler)
@ -66,27 +66,24 @@ func (self *BBSServer) subscribeHandler(w http.ResponseWriter, r *http.Request)
return return
} }
msgc := make(chan models.SocketData)
// attempt to initialize adapters
adapters := make([]adapter.Adapter, 0, 4)
// keep reference to the adapters so we can execute commands on them later
ctx := r.Context() ctx := r.Context()
ctx = c.CloseRead(ctx) ctx = c.CloseRead(ctx)
s := &Subscriber{ s := &Subscriber{
key: cookie.GenToken(64), key: cookie.GenToken(64),
msgs: make(chan []byte, self.subscribeMessageBuffer), msgs: make(chan []byte, self.subscribeMessageBuffer),
data: make(chan models.SocketData),
closeSlow: func() { closeSlow: func() {
c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages")
}, },
} }
// start with an empty set of adapters
// we'll configure them separately
adapters := make([]adapter.Adapter, 0, 4)
self.addSubscriber(s, adapters) self.addSubscriber(s, adapters)
fmt.Println("subscriber added to map") // defer cleanup and write messages in the background
defer self.deleteSubscriber(s) defer self.deleteSubscriber(s)
defer c.Close(websocket.StatusInternalError, "") defer c.Close(websocket.StatusInternalError, "")
@ -102,11 +99,11 @@ func (self *BBSServer) subscribeHandler(w http.ResponseWriter, r *http.Request)
} }
}() }()
fmt.Println("giving subscriber their key: " + s.key) // give user their key
s.msgs <- []byte("{ 'key':'" + s.key + " }") s.msgs <- []byte("{ 'key':'" + s.key + "' }")
fmt.Println("subscriber key sent, listening on the data channel")
listen([]chan models.SocketData{msgc}, s.msgs) // block on the data channel, serializing and passing the data to the subscriber
listen([]chan models.SocketData{s.data}, s.msgs)
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return return