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
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
56
cli/cli.go
56
cli/cli.go
|
@ -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
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> {
|
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,
|
||||||
|
|
|
@ -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
1
go.mod
|
@ -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
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/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=
|
||||||
|
|
|
@ -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`
|
||||||
}
|
}
|
||||||
|
|
109
server/api.go
109
server/api.go
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
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(
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
23
underbbs.go
23
underbbs.go
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue