2021-01-25 10:39:05 +00:00
// SPDX-License-Identifier: MIT
2021-01-25 12:23:03 +00:00
// go-roomsrv hosts the database and p2p server for replication.
2021-01-25 10:39:05 +00:00
// It supplies various flags to contol options.
2021-01-25 12:23:03 +00:00
// See 'go-roomsrv -h' for a list and their usage.
2021-01-25 10:39:05 +00:00
package main
import (
"context"
"encoding/base64"
"flag"
"fmt"
2021-01-25 12:23:03 +00:00
"net"
2021-01-25 10:39:05 +00:00
"os"
"os/signal"
"os/user"
"path/filepath"
2021-01-25 12:23:03 +00:00
"strings"
2021-01-25 10:39:05 +00:00
"syscall"
"time"
// debug
"net/http"
_ "net/http/pprof"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
2021-02-08 16:47:42 +00:00
_ "github.com/mattn/go-sqlite3"
2021-03-26 12:12:44 +00:00
"github.com/throttled/throttled/v2"
"github.com/throttled/throttled/v2/store/memstore"
2021-02-16 13:12:59 +00:00
"github.com/unrolled/secure"
2021-01-25 12:23:03 +00:00
"go.cryptoscope.co/muxrpc/v2/debug"
2021-03-29 10:23:11 +00:00
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
2021-02-09 16:38:51 +00:00
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
2021-03-24 17:31:37 +00:00
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
2021-03-24 09:58:32 +00:00
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
2021-03-12 09:53:43 +00:00
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/sqlite"
2021-02-09 16:38:51 +00:00
"github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
mksrv "github.com/ssb-ngi-pointer/go-ssb-room/roomsrv"
"github.com/ssb-ngi-pointer/go-ssb-room/web/handlers"
2021-01-25 10:39:05 +00:00
)
var (
// flags
flagDisableUNIXSock bool
2021-02-04 10:36:02 +00:00
listenAddrShsMux string
listenAddrHTTP string
2021-02-16 13:12:59 +00:00
httpsDomain string
2021-04-19 12:52:12 +00:00
aliasesAsSubdomains bool
2021-02-04 10:36:02 +00:00
listenAddrDebug string
logToFile string
repoDir string
2021-01-25 10:39:05 +00:00
2021-03-31 13:58:42 +00:00
privacyMode = roomdb . ModeUnknown
2021-03-24 09:58:32 +00:00
2021-01-25 10:39:05 +00:00
// helper
log kitlog . Logger
// juicy bits
appKey string
)
// Version and Build are set by ldflags
var (
Version = "snapshot"
Build = ""
flagPrintVersion bool
)
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)" )
2021-02-04 10:36:02 +00:00
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" )
2021-01-25 10:39:05 +00:00
flag . BoolVar ( & flagDisableUNIXSock , "nounixsock" , false , "disable the UNIX socket RPC interface" )
2021-01-28 14:06:51 +00:00
flag . StringVar ( & repoDir , "repo" , filepath . Join ( u . HomeDir , ".ssb-go-room" ) , "where to put the log and indexes" )
2021-01-25 10:39:05 +00:00
2021-02-04 10:36:02 +00:00
flag . StringVar ( & listenAddrDebug , "dbg" , "localhost:6078" , "listen addr for metrics and pprof HTTP server" )
2021-02-04 13:13:52 +00:00
flag . StringVar ( & logToFile , "logs" , "" , "where to write debug output to (default is just stderr)" )
2021-01-25 10:39:05 +00:00
2021-02-16 13:12:59 +00:00
flag . StringVar ( & httpsDomain , "https-domain" , "" , "which domain to use for TLS and AllowedHosts checks" )
2021-01-25 10:39:05 +00:00
flag . BoolVar ( & flagPrintVersion , "version" , false , "print version number and build date" )
2021-03-24 09:58:32 +00:00
flag . Func ( "mode" , "the privacy mode (values: open, community, restricted) determining room access controls" , func ( val string ) error {
2021-03-31 13:58:42 +00:00
pm := roomdb . ParsePrivacyMode ( val )
err := pm . IsValid ( )
2021-03-24 09:58:32 +00:00
if err != nil {
return fmt . Errorf ( "%s, valid values are open, community, restricted" , err )
}
2021-03-31 13:58:42 +00:00
privacyMode = pm
2021-03-24 09:58:32 +00:00
return nil
} )
2021-04-19 12:52:12 +00:00
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)" )
2021-01-25 10:39:05 +00:00
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...
}
2021-01-25 12:23:03 +00:00
log = kitlog . NewJSONLogger ( kitlog . NewSyncWriter ( logFile ) )
2021-01-25 10:39:05 +00:00
} else {
2021-01-25 12:23:03 +00:00
log = kitlog . NewLogfmtLogger ( os . Stderr )
2021-01-25 10:39:05 +00:00
}
}
2021-01-25 12:23:03 +00:00
func runroomsrv ( ) error {
2021-01-25 10:39:05 +00:00
initFlags ( )
if flagPrintVersion {
log . Log ( "version" , Version , "build" , Build )
return nil
}
2021-03-23 15:50:25 +00:00
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 ] )
}
2021-03-26 19:08:13 +00:00
httpsDomain = "localhost"
2021-02-16 13:12:59 +00:00
}
2021-03-09 17:27:44 +00:00
// 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 )
}
2021-04-19 12:52:12 +00:00
_ , err = net . LookupPort ( "tcp" , muxrpcPortStr )
2021-03-09 17:27:44 +00:00
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 )
}
2021-01-25 10:39:05 +00:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
2021-02-04 13:13:52 +00:00
defer cancel ( )
2021-01-25 10:39:05 +00:00
ak , err := base64 . StdEncoding . DecodeString ( appKey )
if err != nil {
2021-03-09 17:27:44 +00:00
return fmt . Errorf ( "secret-handshake appkey is invalid base64: %w" , err )
2021-01-25 10:39:05 +00:00
}
2021-01-25 12:23:03 +00:00
opts := [ ] roomsrv . Option {
roomsrv . WithLogger ( log ) ,
roomsrv . WithAppKey ( ak ) ,
roomsrv . WithRepoPath ( repoDir ) ,
2021-01-25 17:41:36 +00:00
roomsrv . WithUNIXSocket ( ! flagDisableUNIXSock ) ,
2021-01-25 12:23:03 +00:00
}
2021-01-25 10:39:05 +00:00
2021-01-25 12:23:03 +00:00
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 )
} ) )
}
2021-01-25 10:39:05 +00:00
2021-02-04 10:36:02 +00:00
if listenAddrDebug != "" {
2021-01-25 10:39:05 +00:00
go func ( ) {
// http.Handle("/metrics", promhttp.Handler())
2021-02-04 10:36:02 +00:00
level . Debug ( log ) . Log ( "starting" , "metrics" , "addr" , listenAddrDebug )
err := http . ListenAndServe ( listenAddrDebug , nil )
2021-01-25 10:39:05 +00:00
checkAndLog ( err )
} ( )
}
2021-02-11 15:43:19 +00:00
r := repo . New ( repoDir )
2021-04-19 12:52:12 +00:00
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 ,
}
2021-03-31 09:29:36 +00:00
// open the sqlite version of the roomdb
2021-02-11 15:43:19 +00:00
db , err := sqlite . Open ( r )
if err != nil {
return fmt . Errorf ( "failed to initiate database: %w" , err )
}
2021-03-24 15:52:58 +00:00
bridge := signinwithssb . NewSignalBridge ( )
2021-03-31 13:58:42 +00:00
// the privacy mode flag was passed => update it in the database
if privacyMode != roomdb . ModeUnknown {
db . Config . SetPrivacyMode ( ctx , privacyMode )
}
2021-03-24 15:52:58 +00:00
2021-02-04 13:13:52 +00:00
// create the shs+muxrpc server
2021-03-12 09:53:43 +00:00
roomsrv , err := mksrv . New (
2021-03-19 10:52:33 +00:00
db . Members ,
2021-04-07 08:53:57 +00:00
db . DeniedKeys ,
2021-03-12 09:53:43 +00:00
db . Aliases ,
2021-03-24 10:39:08 +00:00
db . AuthWithSSB ,
2021-03-24 15:52:58 +00:00
bridge ,
2021-03-31 09:29:36 +00:00
db . Config ,
2021-04-19 12:52:12 +00:00
networkInfo ,
2021-03-12 09:53:43 +00:00
opts ... )
2021-01-25 10:39:05 +00:00
if err != nil {
2021-02-04 10:36:02 +00:00
return fmt . Errorf ( "failed to instantiate ssb server: %w" , err )
2021-01-25 10:39:05 +00:00
}
2021-02-08 11:57:14 +00:00
// open the HTTP listener
httpLis , err := net . Listen ( "tcp" , listenAddrHTTP )
if err != nil {
return fmt . Errorf ( "failed to open listener for HTTPdashboard: %w" , err )
}
2021-01-25 10:39:05 +00:00
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 ( )
2021-01-25 12:23:03 +00:00
roomsrv . Shutdown ( )
2021-02-08 11:57:14 +00:00
httpLis . Close ( )
2021-01-25 10:39:05 +00:00
time . Sleep ( 2 * time . Second )
2021-01-25 12:23:03 +00:00
err := roomsrv . Close ( )
2021-01-25 10:39:05 +00:00
checkAndLog ( err )
time . Sleep ( 2 * time . Second )
os . Exit ( 0 )
} ( )
2021-02-04 13:13:52 +00:00
// setup web dashboard handlers
2021-03-26 12:12:44 +00:00
webHandler , err := handlers . New (
2021-02-11 15:23:22 +00:00
kitlog . With ( log , "package" , "web" ) ,
2021-02-08 11:57:14 +00:00
repo . New ( repoDir ) ,
2021-04-19 12:52:12 +00:00
networkInfo ,
2021-02-10 14:50:36 +00:00
roomsrv . StateManager ,
2021-03-17 09:46:05 +00:00
roomsrv . Network ,
2021-03-24 15:52:58 +00:00
bridge ,
2021-03-05 10:15:36 +00:00
handlers . Databases {
2021-03-12 13:42:30 +00:00
Aliases : db . Aliases ,
2021-03-05 10:15:36 +00:00
AuthFallback : db . AuthFallback ,
2021-03-17 09:46:05 +00:00
AuthWithSSB : db . AuthWithSSB ,
2021-03-31 09:29:36 +00:00
Config : db . Config ,
2021-03-19 13:02:19 +00:00
DeniedKeys : db . DeniedKeys ,
2021-03-05 10:15:36 +00:00
Invites : db . Invites ,
Notices : db . Notices ,
2021-03-19 11:28:14 +00:00
Members : db . Members ,
2021-03-05 10:15:36 +00:00
PinnedNotices : db . PinnedNotices ,
} ,
2021-02-08 11:57:14 +00:00
)
2021-02-04 13:13:52 +00:00
if err != nil {
return fmt . Errorf ( "failed to create HTTPdashboard handler: %w" , err )
}
2021-02-16 13:12:59 +00:00
// setup CSP and HTTPS redirects
secureMiddleware := secure . New ( secure . Options {
IsDevelopment : development ,
2021-04-16 14:57:50 +00:00
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 ,
2021-02-16 13:12:59 +00:00
// 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
2021-03-24 15:52:58 +00:00
// helpful: https://report-uri.com/home/generate
ContentSecurityPolicy : "default-src 'self'; img-src 'self' data:" , // enforce no external content
2021-02-16 13:12:59 +00:00
BrowserXssFilter : true ,
FrameDeny : true ,
//ContentTypeNosniff: true, // TODO: fix Content-Type headers served from assets
} )
2021-02-04 15:52:55 +00:00
2021-03-26 12:12:44 +00:00
// 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 )
2021-04-05 09:33:28 +00:00
httpHandler = roomsrv . Network . WebsockHandler ( httpHandler )
2021-03-26 12:12:44 +00:00
// all init was successfull
2021-02-04 10:36:02 +00:00
level . Info ( log ) . Log (
"event" , "serving" ,
"ID" , roomsrv . Whoami ( ) . Ref ( ) ,
"shsmuxaddr" , listenAddrShsMux ,
2021-02-04 13:13:52 +00:00
"httpaddr" , listenAddrHTTP ,
2021-02-04 10:36:02 +00:00
"version" , Version ,
"build" , Build ,
)
2021-02-04 13:13:52 +00:00
// start serving http connections
go func ( ) {
2021-02-08 11:57:14 +00:00
srv := http . Server {
Addr : httpLis . Addr ( ) . String ( ) ,
// Good practice to set timeouts to avoid Slowloris attacks.
2021-03-26 12:12:44 +00:00
// 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 ,
2021-02-08 11:57:14 +00:00
2021-03-26 12:12:44 +00:00
Handler : httpHandler ,
2021-02-08 11:57:14 +00:00
}
err = srv . Serve ( httpLis )
2021-02-04 13:13:52 +00:00
if err != nil {
level . Error ( log ) . Log ( "event" , "http serve failed" , "err" , err )
}
} ( )
// start serving shs+muxrpc connections
2021-01-25 10:39:05 +00:00
for {
// Note: This is where the serving starts ;)
2021-01-25 12:23:03 +00:00
err = roomsrv . Network . Serve ( ctx )
2021-01-25 10:39:05 +00:00
if err != nil {
2021-01-25 12:23:03 +00:00
level . Warn ( log ) . Log ( "event" , "roomsrv node.Serve returned" , "err" , err )
2021-01-25 10:39:05 +00:00
}
time . Sleep ( 1 * time . Second )
select {
case <- ctx . Done ( ) :
2021-01-25 12:23:03 +00:00
err := roomsrv . Close ( )
2021-01-25 10:39:05 +00:00
return err
default :
}
}
}
func main ( ) {
2021-01-25 12:23:03 +00:00
if err := runroomsrv ( ) ; err != nil {
2021-02-11 15:23:22 +00:00
fmt . Fprintf ( os . Stderr , "go-ssb-room: %s\n" , err )
2021-01-25 10:39:05 +00:00
os . Exit ( 1 )
}
}
2021-03-26 12:12:44 +00:00
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 ( )
}