implement authorized fetch and cors
This commit is contained in:
parent
6bb5eff11b
commit
dd27ffabaa
12 changed files with 357 additions and 208 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
56
cli/cli.go
56
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
|
||||
|
|
55
config/config.go
Normal file
55
config/config.go
Normal 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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
1
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -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`
|
||||
}
|
||||
|
|
199
server/api.go
199
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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
23
underbbs.go
23
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,
|
||||
|
|
Loading…
Reference in a new issue