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 package adapter
import ( import (
"crypto"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -10,7 +11,7 @@ import (
"time" "time"
"forge.lightcrystal.systems/nilix/underbbs/models" "forge.lightcrystal.systems/nilix/underbbs/models"
_ "github.com/go-fed/httpsig" "github.com/go-fed/httpsig"
) )
type anonAPAdapter struct { type anonAPAdapter struct {
@ -19,6 +20,8 @@ type anonAPAdapter struct {
client http.Client client http.Client
protocol string protocol string
nickname string nickname string
actorKey *crypto.PrivateKey
actorURL *string
} }
type apLink struct { type apLink struct {
@ -47,15 +50,25 @@ type apIcon struct {
Url string Url string
} }
type apKey struct {
Id string
Owner string
PublicKeyPem string
}
type apActor struct { type apActor struct {
Icon apIcon Icon *apIcon `json:"icon",omitempty`
Id string Id string
Name string Name string
PreferredUsername string PreferredUsername string
Summary string Summary string
Url string Url string
PublicKey ApKey
} }
type ApActor apActor
type ApKey apKey
type apOutbox struct { type apOutbox struct {
OrderedItems []interface{} `json:"-"` OrderedItems []interface{} `json:"-"`
RawOrderedItems []json.RawMessage `json:"OrderedItems"` RawOrderedItems []json.RawMessage `json:"OrderedItems"`
@ -126,14 +139,28 @@ type webFinger struct {
Links []apLink 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.data = data
self.server = server self.server = server
self.nickname = nickname self.nickname = nickname
self.protocol = protocol self.protocol = protocol
self.actorKey = apSigner
self.actorURL = apActorURL
return nil 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 { func getBodyJson(res *http.Response) []byte {
l := res.ContentLength l := res.ContentLength
// if the length is unknown, we'll allocate 4k // 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\"") 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) return self.client.Do(req)
} }

View file

@ -2,6 +2,7 @@ package adapter
import ( import (
"bytes" "bytes"
"crypto"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -49,6 +50,8 @@ type HonkAdapter struct {
username string username string
password string password string
token string token string
apSigner *crypto.PrivateKey
apDomain *string
cache map[string]time.Time cache map[string]time.Time
maxId int maxId int
@ -80,6 +83,7 @@ func (self *HonkAdapter) Init(settings Settings, data *chan SocketData) error {
self.username = parts[1] self.username = parts[1]
self.server = "https://" + parts[2] self.server = "https://" + parts[2]
self.nickname = settings.Nickname self.nickname = settings.Nickname
self.apSigner = settings.ApSigner
if settings.Password == nil || *settings.Password == "" { if settings.Password == nil || *settings.Password == "" {
// we're anonymous! // 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 { func (self *HonkAdapter) Fetch(etype string, ids []string) error {
// honk API is limited, we fall back to the anonymous adapter for fetch ops // honk API is limited, we fall back to the anonymous adapter for fetch ops
aaa := anonAPAdapter{} 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) return aaa.Fetch(etype, ids)
} }

View file

@ -1,71 +1,21 @@
package cli package cli
import ( import (
"encoding/json"
"errors" "errors"
"io/ioutil" "io/ioutil"
"log" "log"
"os"
"path/filepath"
"runtime"
"strings" "strings"
"forge.lightcrystal.systems/nilix/underbbs/adapter" "forge.lightcrystal.systems/nilix/underbbs/adapter"
"forge.lightcrystal.systems/nilix/underbbs/models" "forge.lightcrystal.systems/nilix/underbbs/models"
) )
func GetConfigLocation() string { func Process(gsettings models.GlobalSettings, args ...string) error {
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
adapterName := args[0] adapterName := args[0]
args = args[1:] args = args[1:]
var s *models.Settings
// get config from config fle based on adapter for _, x := range gsettings.Adapters {
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 {
if x.Nickname == adapterName { if x.Nickname == adapterName {
s = &x s = &x
break 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> { async function authorizedFetch(method: string, uri: string, body: any): Promise<Response> {
const headers = new Headers() const headers = new Headers()
headers.set('Authorization', 'Bearer ' + DatagramSocket.skey) headers.set('X-Underbbs-Subscriber', DatagramSocket.skey ?? "")
return await fetch(uri, { return await fetch(uri, {
method: method, method: method,
headers: headers, headers: headers,

View file

@ -30,7 +30,7 @@ export class DatagramSocket {
console.log(data); console.log(data);
if (data.key) { if (data.key) {
DatagramSocket.skey = 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=> { .then(r=> {
if (r.ok) { if (r.ok) {
// iterate through any components which might want to fetch data // iterate through any components which might want to fetch data
@ -96,13 +96,17 @@ export class DatagramSocket {
} }
static connect(gateway: string): void { static connect(gateway: string): void {
DatagramSocket._gateway = gateway; DatagramSocket._gateway = gateway;
const wsProto = location.protocol == "https:" ? "wss" : "ws"; const wsProto = gateway.startsWith("https://") ? "wss" : "ws";
let wsUrl = "" let wsUrl = ""
if (!gateway) { if (!gateway) {
wsUrl = `${wsProto}://${location.host}/subscribe`; wsUrl = `${wsProto}://${location.host}/subscribe`;
} else { } else {
if (gateway.startsWith("https:")) {
gateway = gateway.split("https://")[1];
}
wsUrl = wsProto + "://" + gateway + "/subscribe" wsUrl = wsProto + "://" + gateway + "/subscribe"
} }
const _conn = new WebSocket(wsUrl, "underbbs"); 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/McKael/madon v2.3.0+incompatible
github.com/go-fed/httpsig v1.1.0 github.com/go-fed/httpsig v1.1.0
github.com/nbd-wtf/go-nostr v0.31.2 github.com/nbd-wtf/go-nostr v0.31.2
github.com/rs/cors v1.11.1
github.com/yitsushi/go-misskey v1.1.6 github.com/yitsushi/go-misskey v1.1.6
golang.org/x/time v0.5.0 golang.org/x/time v0.5.0
nhooyr.io/websocket v1.8.11 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/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 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= 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 h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=

View file

@ -1,5 +1,16 @@
package models 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 { type Settings struct {
Nickname string Nickname string
Protocol string Protocol string
@ -9,4 +20,5 @@ type Settings struct {
ApiKey *string `json:"apiKey",omitempty` ApiKey *string `json:"apiKey",omitempty`
Handle *string `json:"handle",omitempty` Handle *string `json:"handle",omitempty`
Password *string `json:"password",omitempty` Password *string `json:"password",omitempty`
ApSigner *crypto.PrivateKey `json:"_",omitempty`
} }

View file

@ -1,6 +1,9 @@
package server package server
import ( import (
"bytes"
"crypto"
"crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
@ -16,6 +19,11 @@ import (
"strings" "strings"
) )
type PrivKeyAux interface {
Public() crypto.PublicKey
Equal(x crypto.PrivateKey) bool
}
type doRequest struct { type doRequest struct {
Action string `json:"action"` Action string `json:"action"`
Content *string `json:"content,omitempty"` Content *string `json:"content,omitempty"`
@ -23,12 +31,8 @@ type doRequest struct {
Desc *string `json:"desc,omitempty"` Desc *string `json:"desc,omitempty"`
} }
func getSubscriberKey(req *http.Request) (string, error) { func getSubscriberKey(req *http.Request) string {
authHeader := req.Header.Get("Authorization") return req.Header.Get("X-Underbbs-Subscriber")
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 { 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 { func setAdaptersForSubscriber(key string, adapters []adapter.Adapter, subscribers map[*Subscriber][]adapter.Adapter) error {
var ptr *Subscriber = nil var ptr *Subscriber = nil
log.Print("looking for subscriber in map...")
for s, _ := range subscribers { for s, _ := range subscribers {
if s.key == key { if s.key == key {
ptr = s ptr = s
} }
} }
if ptr != nil { if ptr != nil {
log.Print("setting adaters for the found subscriber: " + ptr.key)
subscribers[ptr] = adapters subscribers[ptr] = adapters
return nil return nil
} }
return errors.New("subscriber not present in map") 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) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// get subscriber key skey := getSubscriberKey(req)
skey, err := getSubscriberKey(req)
if err != nil {
w.WriteHeader(500)
return
}
subscriber := getSubscriberByKey(skey, subscribers) subscriber := getSubscriberByKey(skey, subscribers)
if subscriber == nil { if subscriber == nil {
w.WriteHeader(404) w.WriteHeader(404)
return return
} }
// decode adapter config from request body // decode adapter config from request body
settings := make([]models.Settings, 0) settings := make([]models.Settings, 0)
err = json.NewDecoder(req.Body).Decode(&settings) err := json.NewDecoder(req.Body).Decode(&settings)
if err != nil { if err != nil {
w.WriteHeader(400) w.WriteHeader(400)
next.ServeHTTP(w, req) next.ServeHTTP(w, req)
@ -86,10 +84,13 @@ func apiConfigureAdapters(next http.Handler, subscribers map[*Subscriber][]adapt
a = &adapter.NostrAdapter{} a = &adapter.NostrAdapter{}
case "mastodon": case "mastodon":
a = &adapter.MastoAdapter{} a = &adapter.MastoAdapter{}
s.ApSigner = apKey
case "misskey": case "misskey":
a = &adapter.MisskeyAdapter{} a = &adapter.MisskeyAdapter{}
s.ApSigner = apKey
case "honk": case "honk":
a = &adapter.HonkAdapter{} a = &adapter.HonkAdapter{}
s.ApSigner = apKey
default: default:
break break
@ -129,12 +130,7 @@ type subscribeParams struct {
func apiAdapterSubscribe(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { func apiAdapterSubscribe(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 := getSubscriberKey(req)
skey, err := getSubscriberKey(req)
if err != nil {
w.WriteHeader(500)
return
}
subscriber := getSubscriberByKey(skey, subscribers) subscriber := getSubscriberByKey(skey, subscribers)
if subscriber == nil { if subscriber == nil {
w.WriteHeader(404) w.WriteHeader(404)
@ -146,7 +142,11 @@ func apiAdapterSubscribe(next http.Handler, subscribers map[*Subscriber][]adapte
urlParams := req.Context().Value("params").(map[string]string) urlParams := req.Context().Value("params").(map[string]string)
adapter := urlParams["adapter_id"] adapter := urlParams["adapter_id"]
sp := subscribeParams{} 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 { for _, a := range adapters {
if a.Name() == adapter { if a.Name() == adapter {
fmt.Printf("adapter.subscribe call: %s {%s, %s}\n", adapter, sp.Filter, *sp.Target) fmt.Printf("adapter.subscribe call: %s {%s, %s}\n", adapter, sp.Filter, *sp.Target)
@ -158,30 +158,24 @@ func apiAdapterSubscribe(next http.Handler, subscribers map[*Subscriber][]adapte
} }
} }
w.WriteHeader(404) w.WriteHeader(404)
next.ServeHTTP(w, req)
}) })
} }
func ProtectWithSubscriberKey(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) 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) {
authHeader := req.Header.Get("Authorization") skey := getSubscriberKey(req)
if strings.HasPrefix(authHeader, "Bearer ") { if getSubscriberByKey(skey, subscribers) != nil {
subscriberKey := strings.Split(authHeader, "Bearer ")[1]
if getSubscriberByKey(subscriberKey, subscribers) != nil {
next.ServeHTTP(w, req) next.ServeHTTP(w, req)
return return
} }
}
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
}) })
} }
func apiAdapterFetch(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { func apiAdapterFetch(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) {
authHeader := req.Header.Get("Authorization") skey := getSubscriberKey(req)
if strings.HasPrefix(authHeader, "Bearer ") { s := getSubscriberByKey(skey, subscribers)
subscriberKey := strings.Split(authHeader, "Bearer ")[1]
s := getSubscriberByKey(subscriberKey, subscribers)
if s != nil { if s != nil {
apiParams := req.Context().Value("params").(map[string]string) apiParams := req.Context().Value("params").(map[string]string)
queryParams := req.URL.Query() queryParams := req.URL.Query()
@ -199,17 +193,13 @@ func apiAdapterFetch(next http.Handler, subscribers map[*Subscriber][]adapter.Ad
} }
} }
} }
}
w.WriteHeader(http.StatusUnauthorized)
}) })
} }
func apiAdapterDo(next http.Handler, subscribers map[*Subscriber][]adapter.Adapter) http.Handler { func apiAdapterDo(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) {
authHeader := req.Header.Get("Authorization") skey := getSubscriberKey(req)
if strings.HasPrefix(authHeader, "Bearer ") { s := getSubscriberByKey(skey, subscribers)
subscriberKey := strings.Split(authHeader, "Bearer ")[1]
s := getSubscriberByKey(subscriberKey, subscribers)
if s != nil { if s != nil {
apiParams := req.Context().Value("params").(map[string]string) apiParams := req.Context().Value("params").(map[string]string)
for _, a := range subscribers[s] { for _, a := range subscribers[s] {
@ -244,8 +234,41 @@ func apiAdapterDo(next http.Handler, subscribers map[*Subscriber][]adapter.Adapt
} }
} }
} }
})
}
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
} }
w.WriteHeader(http.StatusUnauthorized)
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( rtr.Post("/adapters", ProtectWithSubscriberKey(
apiConfigureAdapters(renderer.JSON("data"), self.subscribers), apiConfigureAdapters(renderer.JSON("data"), self.subscribers, self.apKey),
self.subscribers, self.subscribers,
)) ))
@ -278,5 +301,9 @@ func (self *BBSServer) apiMux() http.Handler {
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) return http.HandlerFunc(rtr.ServeHTTP)
} }

View file

@ -2,11 +2,15 @@ package server
import ( import (
"context" "context"
"crypto"
"crypto/x509"
"encoding/pem"
"errors" "errors"
"forge.lightcrystal.systems/nilix/quartzgun/cookie" "forge.lightcrystal.systems/nilix/quartzgun/cookie"
"forge.lightcrystal.systems/nilix/quartzgun/renderer" "forge.lightcrystal.systems/nilix/quartzgun/renderer"
"forge.lightcrystal.systems/nilix/underbbs/adapter" "forge.lightcrystal.systems/nilix/underbbs/adapter"
"forge.lightcrystal.systems/nilix/underbbs/models" "forge.lightcrystal.systems/nilix/underbbs/models"
_ "github.com/rs/cors"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"io/ioutil" "io/ioutil"
"log" "log"
@ -30,20 +34,70 @@ type BBSServer struct {
serveMux http.ServeMux serveMux http.ServeMux
subscribersLock sync.Mutex subscribersLock sync.Mutex
subscribers map[*Subscriber][]adapter.Adapter subscribers map[*Subscriber][]adapter.Adapter
apKey *crypto.PrivateKey
apDomain *string
} }
func New() *BBSServer { func CORS(next http.Handler) http.Handler {
srvr := &BBSServer{ 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, subscribeMessageBuffer: 16,
logf: log.Printf, logf: log.Printf,
subscribers: make(map[*Subscriber][]adapter.Adapter), 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 // frontend is here
srvr.serveMux.Handle("/app/", http.StripPrefix("/app/", renderer.Subtree("./frontend/dist"))) srvr.serveMux.Handle("/app/", http.StripPrefix("/app/", renderer.Subtree("./frontend/dist")))
// api // api
srvr.serveMux.Handle("/api/", http.StripPrefix("/api", srvr.apiMux())) srvr.serveMux.Handle("/api/", http.StripPrefix("/api", CORS(srvr.apiMux())))
// websocket // websocket
srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler) srvr.serveMux.HandleFunc("/subscribe", srvr.subscribeHandler)

View file

@ -13,6 +13,8 @@ import (
"time" "time"
"forge.lightcrystal.systems/nilix/underbbs/cli" "forge.lightcrystal.systems/nilix/underbbs/cli"
"forge.lightcrystal.systems/nilix/underbbs/config"
"forge.lightcrystal.systems/nilix/underbbs/models"
"forge.lightcrystal.systems/nilix/underbbs/server" "forge.lightcrystal.systems/nilix/underbbs/server"
) )
@ -23,11 +25,18 @@ func main() {
var err error = nil var err error = nil
progname := filepath.Base(args[0]) progname := filepath.Base(args[0])
cfg, err := config.LoadConfig()
if err != nil {
panic(err.Error())
}
switch progname { switch progname {
case "underbbs-cli": case "underbbs-cli":
err = run_cli(args[1:]...) err = run_cli(cfg, args[1:]...)
default: default:
err = run_srvr() err = run_srvr(cfg)
} }
if err != nil { if err != nil {
@ -35,17 +44,17 @@ func main() {
} }
} }
func run_cli(args ...string) error { func run_cli(settings models.GlobalSettings, args ...string) error {
return cli.Process(args...) return cli.Process(settings, args...)
} }
func run_srvr() error { func run_srvr(settings models.GlobalSettings) error {
l, err := net.Listen("tcp", ":"+strconv.FormatInt(int64(9090), 10)) l, err := net.Listen("tcp", ":"+strconv.FormatInt(int64(settings.Port), 10))
if err != nil { if err != nil {
return err return err
} }
bbsServer := server.New() bbsServer := server.New(settings)
s := &http.Server{ s := &http.Server{
Handler: bbsServer, Handler: bbsServer,
ReadTimeout: time.Second * 10, ReadTimeout: time.Second * 10,