implement authorized fetch and cors

This commit is contained in:
Iris Lightshard 2025-01-04 20:45:22 -07:00
parent 6bb5eff11b
commit dd27ffabaa
Signed by: Iris Lightshard
GPG key ID: 688407174966CAF3
12 changed files with 357 additions and 208 deletions

View file

@ -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 {

View file

@ -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)
}

View file

@ -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

55
config/config.go Normal file
View file

@ -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
}

View file

@ -30,7 +30,7 @@ function closeErr(): void {
async function authorizedFetch(method: string, uri: string, body: any): Promise<Response> {
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,

View file

@ -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");

1
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

View file

@ -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`
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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,