go-ssb-room/cmd/server/main.go

445 lines
12 KiB
Go

// SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021
//
// SPDX-License-Identifier: MIT
// go-roomsrv hosts the database and p2p server for replication.
// It supplies various flags to contol options.
// See 'go-roomsrv -h' for a list and their usage.
package main
import (
"context"
"encoding/base64"
"flag"
"fmt"
"net"
"os"
"os/signal"
"os/user"
"path/filepath"
"strings"
"syscall"
"time"
// debug
"net/http"
_ "net/http/pprof"
_ "github.com/mattn/go-sqlite3"
"github.com/ssbc/go-muxrpc/v2/debug"
"github.com/throttled/throttled/v2"
"github.com/throttled/throttled/v2/store/memstore"
"github.com/unrolled/secure"
kitlog "go.mindeco.de/log"
"go.mindeco.de/log/level"
"github.com/ssbc/go-ssb-room/v2/internal/network"
"github.com/ssbc/go-ssb-room/v2/internal/repo"
"github.com/ssbc/go-ssb-room/v2/internal/signinwithssb"
"github.com/ssbc/go-ssb-room/v2/roomdb"
"github.com/ssbc/go-ssb-room/v2/roomdb/sqlite"
"github.com/ssbc/go-ssb-room/v2/roomsrv"
mksrv "github.com/ssbc/go-ssb-room/v2/roomsrv"
"github.com/ssbc/go-ssb-room/v2/web/handlers"
)
// Version and Build are set by ldflags
var (
version = "v2.0.7"
commit = "unset"
)
var (
// flags
flagPrintVersion bool
flagDisableUNIXSock bool
listenAddrShsMux string
listenAddrHTTP string
httpsDomain string
aliasesAsSubdomains bool
listenAddrDebug string
logToFile string
repoDir string
privacyMode = roomdb.ModeUnknown
// helper
log kitlog.Logger
// juicy bits
appKey string
)
func checkFatal(err error) {
checkAndLog(err)
if err != nil {
os.Exit(1)
}
}
func checkAndLog(err error) {
if err != nil {
level.Error(log).Log("event", "fatal error", "err", err)
}
}
func initFlags() {
u, err := user.Current()
checkFatal(err)
flag.StringVar(&appKey, "shscap", "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=", "secret-handshake app-key or capability; should likely not be changed as this makes you part of a different network")
flag.StringVar(&listenAddrShsMux, "lismux", ":8008", "address to listen on for secret-handshake+muxrpc")
flag.StringVar(&listenAddrHTTP, "lishttp", ":3000", "address to listen on for HTTP requests")
flag.BoolVar(&flagDisableUNIXSock, "nounixsock", false, "disable the UNIX socket RPC interface")
flag.StringVar(&repoDir, "repo", filepath.Join(u.HomeDir, ".ssb-go-room"), "where to put the log and indexes")
flag.StringVar(&listenAddrDebug, "dbg", "localhost:6078", "listen addr for metrics and pprof HTTP server")
flag.StringVar(&logToFile, "logs", "", "where to write debug output to (default is just stderr)")
flag.StringVar(&httpsDomain, "https-domain", "", "which domain to use for TLS and AllowedHosts checks")
flag.BoolVar(&flagPrintVersion, "version", false, "print version number and build date")
flag.Func("mode", "the privacy mode (values: open, community, restricted) determining room access controls", func(val string) error {
pm := roomdb.ParsePrivacyMode(val)
err := pm.IsValid()
if err != nil {
return fmt.Errorf("%s, valid values are open, community, restricted", err)
}
privacyMode = pm
return nil
})
flag.BoolVar(&aliasesAsSubdomains, "aliases-as-subdomains", true, "needs to be disabled if a wildcard certificate for the room is not available. (stub until we have the admin/settings page)")
flag.Parse()
if logToFile != "" {
logDir := filepath.Join(repoDir, logToFile)
os.MkdirAll(logDir, 0700) // nearly everything is a log here so..
logFileName := fmt.Sprintf("%s-%s.log",
filepath.Base(os.Args[0]),
time.Now().Format("2006-01-02_15-04"))
logFile, err := os.Create(filepath.Join(logDir, logFileName))
if err != nil {
panic(err) // logging not ready yet...
}
log = kitlog.NewJSONLogger(kitlog.NewSyncWriter(logFile))
} else {
log = kitlog.NewLogfmtLogger(os.Stderr)
}
}
func runroomsrv() error {
initFlags()
if flagPrintVersion {
level.Info(log).Log("version", version, "commit", commit)
return nil
}
if httpsDomain == "" {
if !development {
return fmt.Errorf("https-domain can't be empty. See '%s -h' for a full list of options", os.Args[0])
}
httpsDomain = "localhost"
}
// validate listen addresses to bail out on invalid flag input before doing anything else
_, muxrpcPortStr, err := net.SplitHostPort(listenAddrShsMux)
if err != nil {
return fmt.Errorf("invalid muxrpc listener: %w", err)
}
_, err = net.LookupPort("tcp", muxrpcPortStr)
if err != nil {
return fmt.Errorf("invalid tcp port for muxrpc listener: %w", err)
}
_, portHTTPStr, err := net.SplitHostPort(listenAddrHTTP)
if err != nil {
return fmt.Errorf("invalid http listener: %w", err)
}
portHTTP, err := net.LookupPort("tcp", portHTTPStr)
if err != nil {
return fmt.Errorf("invalid tcp port for muxrpc listener: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ak, err := base64.StdEncoding.DecodeString(appKey)
if err != nil {
return fmt.Errorf("secret-handshake appkey is invalid base64: %w", err)
}
opts := []roomsrv.Option{
roomsrv.WithLogger(log),
roomsrv.WithAppKey(ak),
roomsrv.WithRepoPath(repoDir),
roomsrv.WithUNIXSocket(!flagDisableUNIXSock),
}
if logToFile != "" {
opts = append(opts, roomsrv.WithPostSecureConnWrapper(func(conn net.Conn) (net.Conn, error) {
parts := strings.Split(conn.RemoteAddr().String(), "|")
if len(parts) != 2 {
return conn, nil
}
muxrpcDumpDir := filepath.Join(
repoDir,
logToFile,
parts[1], // key first
parts[0],
)
return debug.WrapDump(muxrpcDumpDir, conn)
}))
}
if listenAddrDebug != "" {
go func() {
// http.Handle("/metrics", promhttp.Handler())
level.Debug(log).Log("starting", "metrics", "addr", listenAddrDebug)
err := http.ListenAndServe(listenAddrDebug, nil)
checkAndLog(err)
}()
}
r := repo.New(repoDir)
keyPair, err := repo.DefaultKeyPair(r)
checkAndLog(err)
opts = append(opts, roomsrv.WithKeyPair(keyPair))
networkInfo := network.ServerEndpointDetails{
Development: development,
Domain: httpsDomain,
PortHTTPS: uint(portHTTP),
RoomID: keyPair.Feed,
ListenAddressMUXRPC: listenAddrShsMux,
UseSubdomainForAliases: aliasesAsSubdomains,
}
// open the sqlite version of the roomdb
db, err := sqlite.Open(r)
if err != nil {
return fmt.Errorf("failed to initiate database: %w", err)
}
bridge := signinwithssb.NewSignalBridge()
// the privacy mode flag was passed => update it in the database
if privacyMode != roomdb.ModeUnknown {
db.Config.SetPrivacyMode(ctx, privacyMode)
}
// create the shs+muxrpc server
roomsrv, err := mksrv.New(
db.Members,
db.DeniedKeys,
db.Aliases,
db.AuthWithSSB,
bridge,
db.Config,
networkInfo,
opts...)
if err != nil {
return fmt.Errorf("failed to instantiate ssb server: %w", err)
}
// open the HTTP listener
httpLis, err := net.Listen("tcp", listenAddrHTTP)
if err != nil {
return fmt.Errorf("failed to open listener for HTTPdashboard: %w", err)
}
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
sig := <-c
level.Warn(log).Log("event", "killed", "msg", "received signal, shutting down", "signal", sig.String())
cancel()
roomsrv.Shutdown()
httpLis.Close()
time.Sleep(2 * time.Second)
err := roomsrv.Close()
checkAndLog(err)
time.Sleep(2 * time.Second)
os.Exit(0)
}()
// setup web dashboard handlers
webHandler, err := handlers.New(
kitlog.With(log, "package", "web"),
repo.New(repoDir),
networkInfo,
roomsrv.StateManager,
roomsrv.Network,
bridge,
handlers.Databases{
Aliases: db.Aliases,
AuthFallback: db.AuthFallback,
AuthWithSSB: db.AuthWithSSB,
Config: db.Config,
DeniedKeys: db.DeniedKeys,
Invites: db.Invites,
Notices: db.Notices,
Members: db.Members,
PinnedNotices: db.PinnedNotices,
},
)
if err != nil {
return fmt.Errorf("failed to create HTTPdashboard handler: %w", err)
}
// setup CSP and HTTPS redirects
secureMiddleware := secure.New(secure.Options{
IsDevelopment: development,
AllowedHosts: []string{
// the normal domain
httpsDomain,
// the domain but as a wildcard match with *. infront
`*\.` + strings.Replace(httpsDomain, ".", `\.`, -1),
},
// for the wildcard matching
AllowedHostsAreRegex: true,
// TLS stuff
SSLRedirect: true,
SSLHost: httpsDomain,
// Important for reverse-proxy setups (when nginx or similar does the TLS termination)
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
HostsProxyHeaders: []string{"X-Forwarded-Host"},
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
STSSeconds: 2592000, // 30 days in seconds (TODO configure?)
STSPreload: false, // don't submit to googles list service (TODO configure?)
// TODO configure (could be needed in special setups where the room is a subdomain of a site)
STSIncludeSubdomains: false,
// See for more https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
// helpful: https://report-uri.com/home/generate
ContentSecurityPolicy: "default-src 'self'; img-src 'self' data:", // enforce no external content
BrowserXssFilter: true,
FrameDeny: true,
//ContentTypeNosniff: true, // TODO: fix Content-Type headers served from assets
})
// HTTP rate limiter
throttleStore, err := memstore.New(65536) // 64k different combinations of limitByPathAndAddr
if err != nil {
return fmt.Errorf("failed to init HTTP rate limiter store: %w", err)
}
quota := throttled.RateQuota{
MaxRate: throttled.PerSec(5), // different requests per second per VaryBy
MaxBurst: 25,
}
limiter, err := throttled.NewGCRARateLimiter(throttleStore, quota)
if err != nil {
return fmt.Errorf("failed to init HTTP rate limiter: %w", err)
}
httpRateLimiter := throttled.HTTPRateLimiter{
RateLimiter: limiter,
VaryBy: limitByPathAndAddr{},
}
// wrap dashboard/alias/invite handler in ratlimiter and security middleware
var httpHandler http.Handler
httpHandler = httpRateLimiter.RateLimit(webHandler)
httpHandler = secureMiddleware.Handler(httpHandler)
httpHandler = roomsrv.Network.WebsockHandler(httpHandler)
// all init was successfull
level.Info(log).Log(
"event", "serving",
"ID", roomsrv.Whoami().String(),
"shsmuxaddr", listenAddrShsMux,
"httpaddr", listenAddrHTTP,
"version", version, "commit", commit,
)
// start serving http connections
go func() {
srv := http.Server{
Addr: httpLis.Addr().String(),
// Good practice to set timeouts to avoid Slowloris attacks.
// Keep in mind that the SSE stuff for "sign-in with ssb" can take a moment, thou
ReadHeaderTimeout: time.Second * 15,
WriteTimeout: time.Minute * 3,
IdleTimeout: time.Minute * 3,
Handler: httpHandler,
}
err = srv.Serve(httpLis)
if err != nil {
level.Error(log).Log("event", "http serve failed", "err", err)
}
}()
// start serving shs+muxrpc connections
for {
// Note: This is where the serving starts ;)
err = roomsrv.Network.Serve(ctx)
if err != nil {
level.Warn(log).Log("event", "roomsrv node.Serve returned", "err", err)
}
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
err := roomsrv.Close()
return err
default:
}
}
}
func main() {
if err := runroomsrv(); err != nil {
fmt.Fprintf(os.Stderr, "go-ssb-room: %s\n", err)
os.Exit(1)
}
}
type limitByPathAndAddr struct{}
func (limitByPathAndAddr) Key(r *http.Request) string {
var k strings.Builder
k.WriteString(r.URL.Path)
k.WriteString("\n")
remoteIP := r.Header.Get("X-Forwarded-For")
if remoteIP == "" {
remoteIP = r.RemoteAddr
}
k.WriteString(remoteIP)
return k.String()
}