This commit is contained in:
Henry 2021-01-25 11:39:05 +01:00
commit f325ed5cf3
8 changed files with 575 additions and 0 deletions

197
cmd/server/main.go Normal file
View File

@ -0,0 +1,197 @@
// SPDX-License-Identifier: MIT
// go-tunsrv hosts the database and p2p server for replication.
// It supplies various flags to contol options.
// See 'go-tunsrv -h' for a list and their usage.
package main
import (
"context"
"encoding/base64"
"flag"
"fmt"
"os"
"os/signal"
"os/user"
"path/filepath"
"syscall"
"time"
// debug
"net/http"
_ "net/http/pprof"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/pkg/errors"
)
var (
// flags
flagDisableUNIXSock bool
listenAddr string
debugAddr string
logToFile string
repoDir string
// 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)")
flag.StringVar(&listenAddr, "l", ":8008", "address to listen on")
flag.BoolVar(&flagDisableUNIXSock, "nounixsock", false, "disable the UNIX socket RPC interface")
flag.StringVar(&repoDir, "repo", filepath.Join(u.HomeDir, ".ssb-go"), "where to put the log and indexes")
flag.StringVar(&debugAddr, "dbg", "localhost:6078", "listen addr for metrics and pprof HTTP server")
flag.StringVar(&logToFile, "path", "", "where to write debug output to (otherwise just stderr)")
flag.BoolVar(&flagPrintVersion, "version", false, "print version number and build date")
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(logFile)
} else {
log = kitlog.NewJSONLogger(os.Stderr)
}
}
func runtunsrv() error {
// DEBUGGING
// runtime.SetMutexProfileFraction(1)
// runtime.SetBlockProfileRate(1)
// DEBUGGING
initFlags()
if flagPrintVersion {
log.Log("version", Version, "build", Build)
return nil
}
ctx, cancel := context.WithCancel(context.Background())
ak, err := base64.StdEncoding.DecodeString(appKey)
if err != nil {
return errors.Wrap(err, "application key")
}
if !flagDisableUNIXSock {
// opts = append(opts, mktunsrv.LateOption(mktunsrv.WithUNIXSocket()))
}
// if dbgLogDir != "" {
// opts = append(opts, mktunsrv.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,
// dbgLogDir,
// parts[1], // key first
// parts[0],
// )
// return debug.WrapDump(muxrpcDumpDir, conn)
// }))
// }
if debugAddr != "" {
go func() {
// http.Handle("/metrics", promhttp.Handler())
log.Log("starting", "metrics", "addr", debugAddr)
err := http.ListenAndServe(debugAddr, nil)
checkAndLog(err)
}()
}
tunsrv, err := makeTunnelServer.New(opts...)
if err != nil {
return errors.Wrap(err, "failed to instantiate ssb server")
}
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()
tunsrv.Shutdown()
time.Sleep(2 * time.Second)
err := tunsrv.Close()
checkAndLog(err)
time.Sleep(2 * time.Second)
os.Exit(0)
}()
level.Info(log).Log("event", "serving", "ID", id.Ref(), "addr", listenAddr, "version", Version, "build", Build)
for {
// Note: This is where the serving starts ;)
err = tunsrv.Network.Serve(ctx)
if err != nil {
level.Warn(log).Log("event", "tunsrv node.Serve returned", "err", err)
}
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
err := tunsrv.Close()
return err
default:
}
}
}
func main() {
if err := runtunsrv(); err != nil {
fmt.Fprintf(os.Stderr, "go-ssb-tunnel: %s\n", err)
os.Exit(1)
}
}

5
errors.go Normal file
View File

@ -0,0 +1,5 @@
package tunnel
import "errors"
var ErrShuttingDown = errors.New("go-ssb-tunnel: shutting down") // this is fine

21
go.mod Normal file
View File

@ -0,0 +1,21 @@
module go.mindeco.de/ssb-tunnel
go 1.15
require (
github.com/go-kit/kit v0.10.0
github.com/keks/nocomment v0.0.0-20181007001506-30c6dcb4a472
github.com/pkg/errors v0.9.1
go.cryptoscope.co/secretstream v1.2.2
go.cryptoscope.co/ssb v0.0.0-20201207161753-31d0f24b7a79
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
)
// We need our internal/extra25519 since agl pulled his repo recently.
// Issue: https://github.com/cryptoscope/ssb/issues/44
// Ours uses a fork of x/crypto where edwards25519 is not an internal package,
// This seemed like the easiest change to port agl's extra25519 to use x/crypto
// Background: https://github.com/agl/ed25519/issues/27#issuecomment-591073699
// The branch in use: https://github.com/cryptix/golang_x_crypto/tree/non-internal-edwards
replace golang.org/x/crypto => github.com/cryptix/golang_x_crypto v0.0.0-20200924101112-886946aabeb8

161
internal/keys/keys.go Normal file
View File

@ -0,0 +1,161 @@
// SPDX-License-Identifier: MIT
// Package keys could be it's own thing between go-ssb and this but not today
package keys
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/keks/nocomment"
"go.cryptoscope.co/secretstream/secrethandshake"
refs "go.mindeco.de/ssb-refs"
)
var SecretPerms = os.FileMode(0600)
type KeyPair struct {
Id *refs.FeedRef
Pair secrethandshake.EdKeyPair
}
// the format of the .ssb/secret file as defined by the js implementations
type ssbSecret struct {
Curve string `json:"curve"`
ID *refs.FeedRef `json:"id"`
Private string `json:"private"`
Public string `json:"public"`
}
// IsValidFeedFormat checks if the passed FeedRef is for one of the two supported formats,
// legacy/crapp or GabbyGrove.
func IsValidFeedFormat(r *refs.FeedRef) error {
if r.Algo != refs.RefAlgoFeedSSB1 && r.Algo != refs.RefAlgoFeedGabby {
return fmt.Errorf("ssb: unsupported feed format:%s", r.Algo)
}
return nil
}
// NewKeyPair generates a fresh KeyPair using the passed io.Reader as a seed.
// Passing nil is fine and will use crypto/rand.
func NewKeyPair(r io.Reader) (*KeyPair, error) {
// generate new keypair
kp, err := secrethandshake.GenEdKeyPair(r)
if err != nil {
return nil, fmt.Errorf("ssb: error building key pair: %w", err)
}
keyPair := KeyPair{
Id: &refs.FeedRef{ID: kp.Public[:], Algo: refs.RefAlgoFeedSSB1},
Pair: *kp,
}
return &keyPair, nil
}
// SaveKeyPair serializes the passed KeyPair to path.
// It errors if path already exists.
func SaveKeyPair(kp *KeyPair, path string) error {
if err := IsValidFeedFormat(kp.Id); err != nil {
return err
}
if _, err := os.Stat(path); err == nil {
return fmt.Errorf("ssb.SaveKeyPair: key already exists:%q", path)
}
err := os.MkdirAll(filepath.Dir(path), 0700)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("failed to create folder for keypair: %w", err)
}
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, SecretPerms)
if err != nil {
return fmt.Errorf("ssb.SaveKeyPair: failed to create file: %w", err)
}
if err := EncodeKeyPairAsJSON(kp, f); err != nil {
return err
}
if err := f.Close(); err != nil {
return fmt.Errorf("ssb.SaveKeyPair: failed to close file: %w", err)
}
return nil
}
// EncodeKeyPairAsJSON serializes the passed Keypair into the writer w
func EncodeKeyPairAsJSON(kp *KeyPair, w io.Writer) error {
var sec = ssbSecret{
Curve: "ed25519",
ID: kp.Id,
Private: base64.StdEncoding.EncodeToString(kp.Pair.Secret[:]) + ".ed25519",
Public: base64.StdEncoding.EncodeToString(kp.Pair.Public[:]) + ".ed25519",
}
err := json.NewEncoder(w).Encode(sec)
if err != nil {
return fmt.Errorf("ssb.EncodeKeyPairAsJSON: encoding failed: %w", err)
}
return nil
}
// LoadKeyPair opens fname, ignores any line starting with # and passes it ParseKeyPair
func LoadKeyPair(fname string) (*KeyPair, error) {
f, err := os.Open(fname)
if err != nil {
if os.IsNotExist(err) {
return nil, err
}
return nil, fmt.Errorf("ssb.LoadKeyPair: could not open key file %s: %w", fname, err)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return nil, fmt.Errorf("ssb.LoadKeyPair: could not stat key file %s: %w", fname, err)
}
if perms := info.Mode().Perm(); perms != SecretPerms {
return nil, fmt.Errorf("ssb.LoadKeyPair: expected key file permissions %s, but got %s", SecretPerms, perms)
}
return ParseKeyPair(nocomment.NewReader(f))
}
// ParseKeyPair json decodes an object from the reader.
// It expects std base64 encoded data under the `private` and `public` fields.
func ParseKeyPair(r io.Reader) (*KeyPair, error) {
var s ssbSecret
if err := json.NewDecoder(r).Decode(&s); err != nil {
return nil, fmt.Errorf("ssb.Parse: JSON decoding failed: %w", err)
}
if err := IsValidFeedFormat(s.ID); err != nil {
return nil, err
}
public, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(s.Public, ".ed25519"))
if err != nil {
return nil, fmt.Errorf("ssb.Parse: base64 decode of public part failed: %w", err)
}
private, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(s.Private, ".ed25519"))
if err != nil {
return nil, fmt.Errorf("ssb.Parse: base64 decode of private part failed: %w", err)
}
pair, err := secrethandshake.NewKeyPair(public, private)
if err != nil {
return nil, fmt.Errorf("ssb.Parse: base64 decode of private part failed: %w", err)
}
ssbkp := KeyPair{
Id: s.ID,
Pair: *pair,
}
return &ssbkp, nil
}

103
internal/repo/secret.go Normal file
View File

@ -0,0 +1,103 @@
// SPDX-License-Identifier: MIT
package repo
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"go.cryptoscope.co/ssb"
refs "go.mindeco.de/ssb-refs"
)
func DefaultKeyPair(r Interface) (*ssb.KeyPair, error) {
secPath := r.GetPath("secret")
keyPair, err := ssb.LoadKeyPair(secPath)
if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("repo: error opening key pair: %w", err)
}
keyPair, err = ssb.NewKeyPair(nil)
if err != nil {
return nil, fmt.Errorf("repo: no keypair but couldn't create one either: %w", err)
}
if err := ssb.SaveKeyPair(keyPair, secPath); err != nil {
return nil, fmt.Errorf("repo: error saving new identity file: %w", err)
}
log.Printf("saved identity %s to %s", keyPair.Id.Ref(), secPath)
}
return keyPair, nil
}
func NewKeyPair(r Interface, name, algo string) (*ssb.KeyPair, error) {
return newKeyPair(r, name, algo, nil)
}
func NewKeyPairFromSeed(r Interface, name, algo string, seed io.Reader) (*ssb.KeyPair, error) {
return newKeyPair(r, name, algo, seed)
}
func newKeyPair(r Interface, name, algo string, seed io.Reader) (*ssb.KeyPair, error) {
var secPath string
if name == "-" {
secPath = r.GetPath("secret")
} else {
secPath = r.GetPath("secrets", name)
err := os.MkdirAll(filepath.Dir(secPath), 0700)
if err != nil && !os.IsExist(err) {
return nil, err
}
}
if algo != refs.RefAlgoFeedSSB1 && algo != refs.RefAlgoFeedGabby { // enums would be nice
return nil, fmt.Errorf("invalid feed refrence algo")
}
if _, err := ssb.LoadKeyPair(secPath); err == nil {
return nil, fmt.Errorf("new key-pair name already taken")
}
keyPair, err := ssb.NewKeyPair(seed)
if err != nil {
return nil, fmt.Errorf("repo: no keypair but couldn't create one either: %w", err)
}
keyPair.Id.Algo = algo
if err := ssb.SaveKeyPair(keyPair, secPath); err != nil {
return nil, fmt.Errorf("repo: error saving new identity file: %w", err)
}
log.Printf("saved identity %s to %s", keyPair.Id.Ref(), secPath)
return keyPair, nil
}
func LoadKeyPair(r Interface, name string) (*ssb.KeyPair, error) {
secPath := r.GetPath("secrets", name)
keyPair, err := ssb.LoadKeyPair(secPath)
if err != nil {
return nil, fmt.Errorf("Load: failed to open %q: %w", secPath, err)
}
return keyPair, nil
}
func AllKeyPairs(r Interface) (map[string]*ssb.KeyPair, error) {
kps := make(map[string]*ssb.KeyPair)
err := filepath.Walk(r.GetPath("secrets"), func(path string, info os.FileInfo, err error) error {
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if info.IsDir() {
return nil
}
if kp, err := ssb.LoadKeyPair(path); err == nil {
kps[filepath.Base(path)] = kp
return nil
}
return nil
})
if err != nil {
return nil, err
}
return kps, nil
}

View File

@ -0,0 +1 @@
package nodejs_test

84
tunsrv/options.go Normal file
View File

@ -0,0 +1,84 @@
package tunsrv
import (
"context"
"fmt"
"strings"
"go.mindeco.de/ssb-tunnel/internal/keys"
"go.mindeco.de/ssb-tunnel/internal/repo"
)
type Option func(srv *Server) error
// WithRepoPath changes where the replication database and blobs are stored.
func WithRepoPath(path string) Option {
return func(s *Server) error {
s.repoPath = path
return nil
}
}
// WithAppKey changes the appkey (aka secret-handshake network cap).
// See https://ssbc.github.io/scuttlebutt-protocol-guide/#handshake for more.
func WithAppKey(k []byte) Option {
return func(s *Server) error {
if n := len(k); n != 32 {
return fmt.Errorf("appKey: need 32 bytes got %d", n)
}
s.appKey = k
return nil
}
}
// WithNamedKeyPair changes from the default `secret` file, useful for testing.
func WithNamedKeyPair(name string) Option {
return func(s *Server) error {
r := repo.New(s.repoPath)
var err error
s.KeyPair, err = repo.LoadKeyPair(r, name)
if err != nil {
return fmt.Errorf("loading named key-pair %q failed: %w", name, err)
}
return nil
}
}
// WithJSONKeyPair expectes a JSON-string as blob and calls ssb.ParseKeyPair on it.
// This is useful if you dont't want to place the keypair on the filesystem.
func WithJSONKeyPair(blob string) Option {
return func(s *Server) error {
var err error
s.KeyPair, err = keys.ParseKeyPair(strings.NewReader(blob))
if err != nil {
return fmt.Errorf("JSON KeyPair decode failed: %w", err)
}
return nil
}
}
// WithKeyPair exepect a initialized ssb.KeyPair. Useful for testing.
func WithKeyPair(kp *keys.KeyPair) Option {
return func(s *Server) error {
s.KeyPair = kp
return nil
}
}
// WithInfo changes the info/warn/debug loging output.
func WithInfo(log kitlog.Logger) Option {
return func(s *Server) error {
s.info = log
return nil
}
}
// WithContext changes the context that is context.Background() by default.
// Handy to setup cancelation against a interup signal like ctrl+c.
// Canceling the context also shuts down indexing. If no context is passed sbot.Shutdown() can be used.
func WithContext(ctx context.Context) Option {
return func(s *Server) error {
s.rootCtx, s.Shutdown = ShutdownContext(ctx)
return nil
}
}

3
tunsrv/server.go Normal file
View File

@ -0,0 +1,3 @@
package tunsrv
type Server struct{}