diff --git a/cmd/go-sbot/config.go b/cmd/go-sbot/config.go index d163e600..60958335 100644 --- a/cmd/go-sbot/config.go +++ b/cmd/go-sbot/config.go @@ -5,112 +5,16 @@ package main import ( - "bytes" "encoding/json" - "errors" "fmt" loglib "log" "os" - "path/filepath" "strconv" - "strings" - "github.com/komkom/toml" - "github.com/ssbc/go-ssb/internal/testutils" - "go.mindeco.de/log/level" + "github.com/ssbc/go-ssb/internal/config-reader" ) -type ConfigBool bool -type SbotConfig struct { - ShsCap string `json:"shscap,omitempty"` - Hmac string `json:"hmac,omitempty"` - Hops uint `json:"hops,omitempty"` - - Repo string `json:"repo,omitempty"` - DebugDir string `json:"debugdir,omitempty"` - - MuxRPCAddress string `json:"lis,omitempty"` - WebsocketAddress string `json:"wslis,omitempty"` - WebsocketTLSCert string `json:"wstlscert,omitempty"` - WebsocketTLSKey string `json:"wstlskey,omitempty"` - MetricsAddress string `json:"debuglis,omitempty"` - - NoUnixSocket ConfigBool `json:"nounixsock"` - EnableAdvertiseUDP ConfigBool `json:"localadv"` - EnableDiscoveryUDP ConfigBool `json:"localdiscov"` - EnableEBT ConfigBool `json:"enable-ebt"` - EnableFirewall ConfigBool `json:"promisc"` - RepairFSBeforeStart ConfigBool `json:"repair"` - - NumPeer uint `json:"numPeer,omitempty"` - NumRepl uint `json:"numRepl,omitempty"` - - presence map[string]interface{} -} - -func (config SbotConfig) Has(flagname string) bool { - _, ok := config.presence[flagname] - return ok -} - -func readConfig(configPath string) (SbotConfig, bool) { - var conf SbotConfig - - conf.presence = make(map[string]interface{}) - - // setup logger if not yet setup (used for tests) - if log == nil { - log = testutils.NewRelativeTimeLogger(nil) - } - - data, err := os.ReadFile(configPath) - if err != nil { - level.Info(log).Log("event", "read config", "msg", "no config detected", "path", configPath) - return conf, false - } - - level.Info(log).Log("event", "read config", "msg", "config detected", "path", configPath) - - // 1) first we unmarshal into struct for type checks - decoder := json.NewDecoder(toml.New(bytes.NewBuffer(data))) - err = decoder.Decode(&conf) - check(err, "decode into struct") - - // 2) then we unmarshal into a map for presence check (to make sure bools are treated correctly) - decoder = json.NewDecoder(toml.New(bytes.NewBuffer(data))) - err = decoder.Decode(&conf.presence) - check(err, "decode into presence map") - - // help repo path's default to align with common user expectations - conf.Repo = expandPath(conf.Repo) - - return conf, true -} - -// ensure the following type of path expansions take place: -// * ~/.ssb => /home//.ssb -// * .ssb => /home//.ssb -// * /stuff/.ssb => /stuff/.ssb -func expandPath(p string) string { - home, err := os.UserHomeDir() - if err != nil { - loglib.Fatalln("could not get user home directory (os.UserHomeDir()") - } - - if strings.HasPrefix(p, "~") { - p = strings.Replace(p, "~", home, 1) - } - - // not relative path, not absolute path => - // place relative to home dir "~/" - if !filepath.IsAbs(p) { - p = filepath.Join(home, p) - } - - return p -} - -func ReadEnvironmentVariables(config *SbotConfig) { +func ReadEnvironmentVariables(config *config.SbotConfig) { if val := os.Getenv("SSB_SECRET_FILE"); val != "" { loglib.Fatalln("flag SSB_SECRET_FILE not implemented") } @@ -130,88 +34,88 @@ func ReadEnvironmentVariables(config *SbotConfig) { // go-ssb specific env flag, for peachcloud/pub compat if val := os.Getenv("GO_SSB_REPAIR_FS"); val != "" { config.RepairFSBeforeStart = readEnvironmentBoolean(val) - config.presence["repair"] = true + config.SetPresence("repair", true) } if val := os.Getenv("SSB_DATA_DIR"); val != "" { config.Repo = val - config.presence["repo"] = true + config.SetPresence("repo", true) } if val := os.Getenv("SSB_LOG_DIR"); val != "" { config.DebugDir = val - config.presence["debugdir"] = true + config.SetPresence("debugdir", true) } if val := os.Getenv("SSB_PROMETHEUS_ADDRESS"); val != "" { config.MetricsAddress = val - config.presence["debuglis"] = true + config.SetPresence("debuglis", true) } if val := os.Getenv("SSB_PROMETHEUS_ENABLED"); val != "" { - config.presence["debuglis"] = readEnvironmentBoolean(val) + config.SetPresence("debuglis", readEnvironmentBoolean(val)) } if val := os.Getenv("SSB_HOPS"); val != "" { hops, err := strconv.Atoi(val) check(err, "parse hops from environment variable") config.Hops = uint(hops) - config.presence["hops"] = true + config.SetPresence("hops", true) } if val := os.Getenv("SSB_MUXRPC_ADDRESS"); val != "" { config.MuxRPCAddress = val - config.presence["lis"] = true + config.SetPresence("lis", true) } if val := os.Getenv("SSB_WS_ADDRESS"); val != "" { config.WebsocketAddress = val - config.presence["wslis"] = true + config.SetPresence("wslis", true) } if val := os.Getenv("SSB_WS_TLS_CERT"); val != "" { config.WebsocketTLSCert = val - config.presence["wstlscert"] = true + config.SetPresence("wstlscert", true) } if val := os.Getenv("SSB_WS_TLS_KEY"); val != "" { config.WebsocketTLSKey = val - config.presence["wstlskey"] = true + config.SetPresence("wstlskey", true) } if val := os.Getenv("SSB_EBT_ENABLED"); val != "" { config.EnableEBT = readEnvironmentBoolean(val) - config.presence["enable-ebt"] = true + config.SetPresence("enable-ebt", true) } if val := os.Getenv("SSB_CONN_FIREWALL_ENABLED"); val != "" { config.EnableFirewall = readEnvironmentBoolean(val) - config.presence["promisc"] = true + config.SetPresence("promisc", true) } if val := os.Getenv("SSB_SOCKET_ENABLED"); val != "" { config.NoUnixSocket = !readEnvironmentBoolean(val) - config.presence["nounixsock"] = true + config.SetPresence("nounixsock", true) } if val := os.Getenv("SSB_CONN_DISCOVERY_UDP_ENABLED"); val != "" { config.EnableDiscoveryUDP = readEnvironmentBoolean(val) - config.presence["localdiscov"] = true + config.SetPresence("localdiscov", true) } if val := os.Getenv("SSB_CONN_BROADCAST_UDP_ENABLED"); val != "" { config.EnableAdvertiseUDP = readEnvironmentBoolean(val) - config.presence["localadv"] = true + config.SetPresence("localadv", true) } if val := os.Getenv("SSB_CAP_SHS_KEY"); val != "" { config.ShsCap = val - config.presence["shscap"] = true + config.SetPresence("shscap", true) } if val := os.Getenv("SSB_CAP_HMAC_KEY"); val != "" { config.Hmac = val - config.presence["hmac"] = true + config.SetPresence("hmac", true) } if val := os.Getenv("SSB_NUM_PEER"); val != "" { @@ -227,53 +131,15 @@ func ReadEnvironmentVariables(config *SbotConfig) { } } -func (booly ConfigBool) MarshalJSON() ([]byte, error) { - temp := (bool)(booly) - b, err := json.Marshal(temp) - return b, err -} - -func (booly *ConfigBool) UnmarshalJSON(b []byte) error { - // unmarshal into interface{} first, as a bool can't be unmarshaled into a string - var v interface{} - err := json.Unmarshal(b, &v) - if err != nil { - return eout(err, "unmarshal config bool") - } - - // go through a type assertion dance, capturing the two cases: - // 1. if the config value is a proper boolean, and - // 2. if the config value is a boolish string (e.g. "true" or "1") - var temp bool - if val, ok := v.(bool); ok { - temp = val - } else if s, ok := v.(string); ok { - temp = booleanIsTrue(s) - if !temp { - // catch strings that cause a false value, but which aren't boolish - if s != "false" && s != "0" && s != "no" && s != "off" { - return errors.New("non-boolean string found when unmarshaling boolish values") - } - } - } - *booly = (ConfigBool)(temp) - - return nil -} - -func booleanIsTrue(s string) bool { - return s == "true" || s == "1" || s == "yes" || s == "on" -} - -func readEnvironmentBoolean(s string) ConfigBool { - var booly ConfigBool +func readEnvironmentBoolean(s string) config.ConfigBool { + var booly config.ConfigBool err := json.Unmarshal([]byte(s), booly) check(err, "parsing environment variable bool") return booly } -func readConfigAndEnv(configPath string) (SbotConfig, bool) { - config, exists := readConfig(configPath) +func readConfigAndEnv(configPath string) (config.SbotConfig, bool) { + config, exists := config.ReadConfigSbot(configPath) ReadEnvironmentVariables(&config) return config, exists } diff --git a/cmd/go-sbot/config_test.go b/cmd/go-sbot/config_test.go index d5fc22a9..2233809e 100644 --- a/cmd/go-sbot/config_test.go +++ b/cmd/go-sbot/config_test.go @@ -17,12 +17,14 @@ import ( "time" "github.com/ssbc/go-ssb/client" + "github.com/ssbc/go-ssb/internal/config-reader" "github.com/stretchr/testify/require" ) func TestMarshalConfigBooleans(t *testing.T) { r := require.New(t) - configContents := `# Supply various flags to control go-sbot options. + configContents := `[go-sbot] +# Supply various flags to control go-sbot options. hops = 2 # Address to listen on @@ -50,7 +52,7 @@ numPeer = 5 # how many feeds can be replicated concurrently using legacy gossip replication numRepl = 10 ` - expectedConfig := SbotConfig{ + expectedConfig := config.SbotConfig{ Hops: 2, MuxRPCAddress: ":8008", WebsocketAddress: ":8989", @@ -70,7 +72,7 @@ numRepl = 10 configPath := filepath.Join(testPath, "config.toml") err := os.WriteFile(configPath, []byte(configContents), 0700) r.NoError(err, "write config file") - configFromDisk, _ := readConfig(configPath) + configFromDisk, _ := config.ReadConfigSbot(configPath) // config values should be read correctly r.EqualValues(expectedConfig.Hops, configFromDisk.Hops) r.EqualValues(expectedConfig.MuxRPCAddress, configFromDisk.MuxRPCAddress) @@ -88,7 +90,7 @@ numRepl = 10 func TestUnmarshalConfig(t *testing.T) { r := require.New(t) - config := SbotConfig{ + config := config.SbotConfig{ NoUnixSocket: true, EnableAdvertiseUDP: true, EnableDiscoveryUDP: true, @@ -117,7 +119,8 @@ func TestUnmarshalConfig(t *testing.T) { func TestConfiguredSbot(t *testing.T) { r := require.New(t) - configContents := `# Supply various flags to control go-sbot options. + configContents := `[go-sbot] +# Supply various flags to control go-sbot options. hops = 2 shscap = "0KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=" @@ -142,7 +145,7 @@ numPeer = 5 # how many feeds can be replicated concurrently using legacy gossip replication numRepl = 10 ` - expectedConfig := SbotConfig{ + expectedConfig := config.SbotConfig{ ShsCap: "0KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=", Hops: 2, MuxRPCAddress: ":8008", @@ -188,7 +191,7 @@ numRepl = 10 runningConfPath := filepath.Join(testPath, "running-config.json") b, err := os.ReadFile(runningConfPath) r.NoError(err) - var runningConfig SbotConfig + var runningConfig config.SbotConfig err = json.Unmarshal(b, &runningConfig) r.NoError(err) @@ -210,7 +213,8 @@ func TestConfigRepoPathExpands(t *testing.T) { r := require.New(t) testRepoConfig := func(repodir, expected, failMsg string) { - configContents := fmt.Sprintf(`repo = "%s"`, repodir) + configContents := fmt.Sprintf(`[go-sbot] +repo = "%s"`, repodir) testPath := filepath.Join(".", "testrun", t.Name()) r.NoError(os.RemoveAll(testPath), "remove testrun folder") @@ -218,7 +222,7 @@ func TestConfigRepoPathExpands(t *testing.T) { configPath := filepath.Join(testPath, "config.toml") err := os.WriteFile(configPath, []byte(configContents), 0700) r.NoError(err, "write config file") - parsedConfig, _ := readConfig(configPath) + parsedConfig, _ := config.ReadConfigSbot(configPath) r.EqualValues(expected, parsedConfig.Repo, failMsg) } @@ -306,6 +310,6 @@ func TestGenerateDefaultConfig(t *testing.T) { _, err = os.Stat(confPath) r.NoError(err) - _, exists := readConfig(confPath) + _, exists := config.ReadConfigSbot(confPath) r.True(exists) } diff --git a/cmd/go-sbot/default-config.toml b/cmd/go-sbot/default-config.toml index b0acb6b0..3f9c2f30 100644 --- a/cmd/go-sbot/default-config.toml +++ b/cmd/go-sbot/default-config.toml @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2023 The Go-SSB Authors # SPDX-License-Identifier: MIT +[go-sbot] # Where to put the log and indexes repo = '.ssb-go' # Where to write debug output: NOTE, this is relative to "repo" atm @@ -40,3 +41,24 @@ enable-ebt = false promisc = false # Disable the UNIX socket RPC interface nounixsock = false + + + +[sbotcli] +# SHS Key (default: 1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=) +shscap = "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=" + +# TCP address of the sbot to connect to (or listen on) (default: localhost:8008) +addr = "localhost:8008" + +# The remote pubkey you are connecting to (by default the local key) +remotekey = "" + +# Secret key file (default: ~/.ssb-go/secret) +key = "~/.ssb-go/secret" + +# If set, Unix socket is used instead of TCP (default: ~/.ssb-go/socket) +unixsock = "~/.ssb-go/socket" + +# Pass a duration (like 3s or 5m) after which it times out (empty string to disable) (default: 45s) +timeout = "45s" diff --git a/cmd/sbotcli/aliases.go b/cmd/sbotcli/aliases.go index a3e20998..8128cb55 100644 --- a/cmd/sbotcli/aliases.go +++ b/cmd/sbotcli/aliases.go @@ -29,7 +29,7 @@ var aliasCmd = &cli.Command{ var aliasRegisterCmd = &cli.Command{ Name: "register", - Usage: "Register a new alias on the remote room (should be used with --remoteKey and --addr)", + Usage: "Register a new alias on the remote room (should be used with --remotekey and --addr)", ArgsUsage: "", Action: func(ctx *cli.Context) error { @@ -48,7 +48,7 @@ var aliasRegisterCmd = &cli.Command{ return err } - roomID, err := refs.ParseFeedRef(ctx.String("remoteKey")) + roomID, err := refs.ParseFeedRef(ctx.String("remotekey")) if err != nil { return err } @@ -74,7 +74,7 @@ var aliasRegisterCmd = &cli.Command{ var aliasRevokeCmd = &cli.Command{ Name: "revoke", - Usage: "Removes the alias from the remote (should be used with --remoteKey and --addr)", + Usage: "Removes the alias from the remote (should be used with --remotekey and --addr)", ArgsUsage: "<%...sha256>", Action: func(ctx *cli.Context) error { ref := ctx.Args().Get(0) diff --git a/cmd/sbotcli/config.go b/cmd/sbotcli/config.go new file mode 100644 index 00000000..646841f0 --- /dev/null +++ b/cmd/sbotcli/config.go @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2023 The Go-SSB Authors +// +// SPDX-License-Identifier: MIT + +package main + +import ( + "encoding/json" + "fmt" + loglib "log" + "os" + + "github.com/ssbc/go-ssb/internal/config-reader" +) + +func ReadEnvironmentVariables(config *config.SbotCliConfig) { + if val := os.Getenv("SSB_CAP_SHS_KEY"); val != "" { + config.ShsCap = val + config.SetPresence("shscap", true) + } + + if val := os.Getenv("SSB_ADDR"); val != "" { + config.Addr = val + config.SetPresence("addr", true) + } + + if val := os.Getenv("SSB_REMOTE_KEY"); val != "" { + config.RemoteKey = val + config.SetPresence("remotekey", true) + } + + if val := os.Getenv("SSB_KEY"); val != "" { + config.Key = val + config.SetPresence("key", true) + } + + if val := os.Getenv("SSB_UNIX_SOCK"); val != "" { + config.UnixSock = val + config.SetPresence("unixsock", true) + } + + if val := os.Getenv("SSB_TIMEOUT"); val != "" { + config.Timeout = val + config.SetPresence("timeout", true) + } +} + +func readEnvironmentBoolean(s string) config.ConfigBool { + var booly config.ConfigBool + err := json.Unmarshal([]byte(s), booly) + configCheck(err, "parsing environment variable bool") + return booly +} + +func readConfigAndEnv(configPath string) (config.SbotCliConfig, bool) { + config, exists := config.ReadConfigSbotCli(configPath) + ReadEnvironmentVariables(&config) + return config, exists +} + +func eout(err error, msg string, args ...interface{}) error { + if err != nil { + msg = fmt.Sprintf(msg, args...) + return fmt.Errorf("%s (%w)", msg, err) + } + return nil +} + +func configCheck(err error, msg string, args ...interface{}) { + if err = eout(err, msg, args...); err != nil { + loglib.Fatalln(err) + } +} diff --git a/cmd/sbotcli/config_test.go b/cmd/sbotcli/config_test.go new file mode 100644 index 00000000..2d564742 --- /dev/null +++ b/cmd/sbotcli/config_test.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 The Go-SSB Authors +// +// SPDX-License-Identifier: MIT + +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/ssbc/go-ssb/internal/config-reader" + "github.com/stretchr/testify/require" +) + +func TestMarshallConfig(t *testing.T) { + r := require.New(t) + configContents := `[sbotcli] +shscap = "C80ZE1AsIGuRehUpiHXCRt3akFJzUTKqXEJ7i30OjNI=" +addr = "localhost:12345" +remotekey = "@QlCTpvY7p9ty2yOFrv1WU1AE88aoQc4Y7wYal7PFc+w=.ed25519" +key = "/tmp/testkey" +unixsock = "/tmp/testsocket" +timeout = "3600s" +` + expectedConfig := config.SbotCliConfig{ + ShsCap: "C80ZE1AsIGuRehUpiHXCRt3akFJzUTKqXEJ7i30OjNI=", + Addr: "localhost:12345", + RemoteKey: "@QlCTpvY7p9ty2yOFrv1WU1AE88aoQc4Y7wYal7PFc+w=.ed25519", + Key: "/tmp/testkey", + UnixSock: "/tmp/testsocket", + Timeout: "3600s", + } + testPath := filepath.Join(".", "testrun", t.Name()) + r.NoError(os.RemoveAll(testPath), "remove testrun folder") + r.NoError(os.MkdirAll(testPath, 0700), "make new testrun folder") + configPath := filepath.Join(testPath, "config.toml") + err := os.WriteFile(configPath, []byte(configContents), 0700) + r.NoError(err, "write config file") + configFromDisk, _ := config.ReadConfigSbotCli(configPath) + // config values should be read correctly + r.EqualValues(expectedConfig.ShsCap, configFromDisk.ShsCap) + r.EqualValues(expectedConfig.Addr, configFromDisk.Addr) + r.EqualValues(expectedConfig.RemoteKey, configFromDisk.RemoteKey) + r.EqualValues(expectedConfig.Key, configFromDisk.Key) + r.EqualValues(expectedConfig.UnixSock, configFromDisk.UnixSock) + r.EqualValues(expectedConfig.Timeout, configFromDisk.Timeout) +} + +func TestUnmarshalConfig(t *testing.T) { + r := require.New(t) + config := config.SbotCliConfig{ + ShsCap: "C80ZE1AsIGuRehUpiHXCRt3akFJzUTKqXEJ7i30OjNI=", + Addr: "localhost:12345", + RemoteKey: "@QlCTpvY7p9ty2yOFrv1WU1AE88aoQc4Y7wYal7PFc+w=.ed25519", + Key: "/tmp/testkey", + UnixSock: "/tmp/testsocket", + Timeout: "3600s", + } + b, err := json.MarshalIndent(config, "", " ") + r.NoError(err) + configStr := string(b) + expectedValues := strings.Split(strings.TrimSpace(`"shscap": "C80ZE1AsIGuRehUpiHXCRt3akFJzUTKqXEJ7i30OjNI=", + "addr": "localhost:12345", + "remotekey": "@QlCTpvY7p9ty2yOFrv1WU1AE88aoQc4Y7wYal7PFc+w=.ed25519", + "key": "/tmp/testkey", + "unixsock": "/tmp/testsocket", + "timeout": "3600s" + `), "\n") + for _, expected := range expectedValues { + r.True(strings.Contains(configStr, expected), expected) + } +} + diff --git a/cmd/sbotcli/main.go b/cmd/sbotcli/main.go index ae7aaef7..6405f5d1 100644 --- a/cmd/sbotcli/main.go +++ b/cmd/sbotcli/main.go @@ -39,6 +39,8 @@ import ( "github.com/ssbc/go-ssb/plugins/legacyinvites" ) +const DEFAULT_GO_SSB_DIR string = ".ssb-go" + // Version and Build are set by ldflags var ( Version = "snapshot" @@ -51,6 +53,7 @@ var ( log kitlog.Logger + configFileFlag = cli.StringFlag{Name: "config", Usage: "path to config file; if filename is omitted from config path config.toml is used"} keyFileFlag = cli.StringFlag{Name: "key,k", Usage: "Secret key file", Value: "unset"} unixSockFlag = cli.StringFlag{Name: "unixsock", Usage: "If set, Unix socket is used instead of TCP"} ) @@ -59,8 +62,9 @@ func init() { u, err := user.Current() check(err) - keyFileFlag.Value = filepath.Join(u.HomeDir, ".ssb-go", "secret") - unixSockFlag.Value = filepath.Join(u.HomeDir, ".ssb-go", "socket") + configFileFlag.Value = filepath.Join(u.HomeDir, DEFAULT_GO_SSB_DIR, "config.toml") + keyFileFlag.Value = filepath.Join(u.HomeDir, DEFAULT_GO_SSB_DIR, "secret") + unixSockFlag.Value = filepath.Join(u.HomeDir, DEFAULT_GO_SSB_DIR, "socket") log = term.NewColorLogger(os.Stderr, kitlog.NewLogfmtLogger, colorFn) } @@ -75,9 +79,10 @@ Please note, global options must be placed before sub-commands, e.g. Version: "alpha4", Flags: []cli.Flag{ + &configFileFlag, &cli.StringFlag{Name: "shscap", Value: "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=", Usage: "SHS key"}, &cli.StringFlag{Name: "addr", Value: "localhost:8008", Usage: "TCP address of the sbot to connect to (or listen on)"}, - &cli.StringFlag{Name: "remoteKey", Value: "", Usage: "The remote pubkey you are connecting to (by default the local key)"}, + &cli.StringFlag{Name: "remotekey", Aliases: []string{"remoteKey"}, Value: "", Usage: "The remote pubkey you are connecting to (by default the local key)"}, &keyFileFlag, &unixSockFlag, &cli.BoolFlag{Name: "verbose,vv", Usage: "Print MUXRPC packets"}, @@ -140,6 +145,43 @@ func todo(ctx *cli.Context) error { } func initClient(ctx *cli.Context) error { + // first, we need to check if we have a config file to pull options from + // if we do, then this is where we would apply those options if they have not been overridden by a command-line flag + config, exists := readConfigAndEnv(ctx.String("config")) + if exists { + if !ctx.IsSet("shscap") { + if config.Has("shscap") { + ctx.Set("shscap", config.ShsCap) + } + } + if !ctx.IsSet("addr") { + if config.Has("addr") { + ctx.Set("addr", config.Addr) + } + } + if !ctx.IsSet("remotekey") { + if config.Has("remotekey") { + ctx.Set("remotekey", config.RemoteKey) + } + } + if !ctx.IsSet("key") { + if config.Has("key") { + ctx.Set("key", config.Key) + } + } + if !ctx.IsSet("unixsock") { + if config.Has("unixsock") { + ctx.Set("unixsock", config.UnixSock) + } + } + if !ctx.IsSet("timeout") { + if config.Has("timeout") { + ctx.Set("timeout", config.Timeout) + } + } + } + + // now, initialize the client dstr := ctx.String("timeout") if dstr != "" { d, err := time.ParseDuration(dstr) @@ -187,12 +229,12 @@ func newTCPClient(ctx *cli.Context) (*ssbClient.Client, error) { var remotePubKey = make(ed25519.PublicKey, ed25519.PublicKeySize) copy(remotePubKey, localKey.ID().PubKey()) - if rk := ctx.String("remoteKey"); rk != "" { + if rk := ctx.String("remotekey"); rk != "" { rk = strings.TrimSuffix(rk, ".ed25519") rk = strings.TrimPrefix(rk, "@") rpk, err := base64.StdEncoding.DecodeString(rk) if err != nil { - return nil, fmt.Errorf("init: base64 decode of --remoteKey failed: %w", err) + return nil, fmt.Errorf("init: base64 decode of --remotekey failed: %w", err) } copy(remotePubKey, rpk) } diff --git a/docs/config.md b/docs/config.md index 0358a5ba..6005ce46 100644 --- a/docs/config.md +++ b/docs/config.md @@ -60,6 +60,7 @@ Please note, a "vendored" default configuration is maintained in up-to-date. ```toml +[go-sbot] # Where to put the log and indexes repo = '.ssb-go' # Where to write debug output: NOTE, this is relative to "repo" atm @@ -99,6 +100,27 @@ enable-ebt = false promisc = false # Disable the UNIX socket RPC interface nounixsock = false + + + +[sbotcli] +# SHS Key (default: 1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=) +shscap = "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=" + +# TCP address of the sbot to connect to (or listen on) (default: localhost:8008) +addr = "localhost:8008" + +# The remote pubkey you are connecting to (by default the local key) +remotekey = "" + +# Secret key file (default: ~/.ssb-go/secret) +key = "~/.ssb-go/secret" + +# If set, Unix socket is used instead of TCP (default: ~/.ssb-go/socket) +unixsock = "~/.ssb-go/socket" + +# Pass a duration (like 3s or 5m) after which it times out (empty string to disable) (default: 45s) +timeout = "45s" ``` ## Environment Variables @@ -113,6 +135,7 @@ SSB_CONFIG_FILE="~/.ssb-go-config.toml" ./go-sbot ### Environment variable listing ```sh +// === for go-ssb === SSB_DATA_DIR="/var/lib/ssb-server" SSB_CONFIG_FILE="/etc/ssb-server/config" SSB_LOG_DIR="/var/log/ssb-server" @@ -141,6 +164,16 @@ GO_SSB_REPAIR_FS=no // SSB_LOG_LEVEL="info" currently not implemented // SSB_CAP_INVITE_KEY="" currently not implemented // SSB_SOCKET_ENABLED=no currently not implemented + + + +// === for sbotcli === +SSB_CAP_SHS_KEY="1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=" +SSB_ADDR="localhost:8008" +SSB_REMOTE_KEY="" +SSB_KEY="~/.ssb-go/secret" +SSB_UNIX_SOCK="~/.ssb-go/socket" +SSB_TIMEOUT="45s" ``` ## Inspecting configured values diff --git a/internal/config-reader/config.go b/internal/config-reader/config.go new file mode 100644 index 00000000..b64d20db --- /dev/null +++ b/internal/config-reader/config.go @@ -0,0 +1,234 @@ +// SPDX-FileCopyrightText: 2023 The Go-SSB Authors +// +// SPDX-License-Identifier: MIT + +package config + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + loglib "log" + "os" + "path/filepath" + "strings" + + "github.com/komkom/toml" + "github.com/ssbc/go-ssb/internal/testutils" + "go.mindeco.de/log/level" +) + +type ConfigBool bool +type SbotConfig struct { + ShsCap string `json:"shscap,omitempty"` + Hmac string `json:"hmac,omitempty"` + Hops uint `json:"hops,omitempty"` + + Repo string `json:"repo,omitempty"` + DebugDir string `json:"debugdir,omitempty"` + + MuxRPCAddress string `json:"lis,omitempty"` + WebsocketAddress string `json:"wslis,omitempty"` + WebsocketTLSCert string `json:"wstlscert,omitempty"` + WebsocketTLSKey string `json:"wstlskey,omitempty"` + MetricsAddress string `json:"debuglis,omitempty"` + + NoUnixSocket ConfigBool `json:"nounixsock"` + EnableAdvertiseUDP ConfigBool `json:"localadv"` + EnableDiscoveryUDP ConfigBool `json:"localdiscov"` + EnableEBT ConfigBool `json:"enable-ebt"` + EnableFirewall ConfigBool `json:"promisc"` + RepairFSBeforeStart ConfigBool `json:"repair"` + + NumPeer uint `json:"numPeer,omitempty"` + NumRepl uint `json:"numRepl,omitempty"` + + presence map[string]interface{} +} +type SbotCliConfig struct { + ShsCap string `json:"shscap,omitempty"` + Addr string `json:"addr,omitempty"` + RemoteKey string `json:"remotekey,omitempty"` + Key string `json:"key,omitempty"` + UnixSock string `json:"unixsock,omitempty"` + Timeout string `json:"timeout,omitempty"` + + presence map[string]interface{} +} + +type MergedConfig struct { + GoSbot SbotConfig `json:"go-sbot"` + SbotCli SbotCliConfig `json:"sbotcli"` +} + +func (config SbotConfig) Has(flagname string) bool { + _, ok := config.presence[flagname] + return ok +} + +func (config SbotConfig) SetPresence(flagname string, val ConfigBool) { + config.presence[flagname] = val +} + +func (config SbotCliConfig) Has(flagname string) bool { + _, ok := config.presence[flagname] + return ok +} + +func (config SbotCliConfig) SetPresence(flagname string, val ConfigBool) { + config.presence[flagname] = val +} + +func ReadConfigSbot(configPath string) (SbotConfig, bool) { + var conf MergedConfig + + // setup logger if not yet setup (used for tests) + log := testutils.NewRelativeTimeLogger(nil) + + data, err := os.ReadFile(configPath) + if err != nil { + level.Info(log).Log("event", "read config", "msg", "no config detected", "path", configPath) + return conf.GoSbot, false + } + + level.Info(log).Log("event", "read config", "msg", "config detected", "path", configPath) + + // 1) first we unmarshal into struct for type checks + decoder := json.NewDecoder(toml.New(bytes.NewBuffer(data))) + err = decoder.Decode(&conf) + check(err, "decode into struct") + + // 2) then we unmarshal into a map for presence check (to make sure bools are treated correctly) + presence := make(map[string]interface{}) + decoder = json.NewDecoder(toml.New(bytes.NewBuffer(data))) + err = decoder.Decode(&presence) + check(err, "decode into presence map") + if presence["go-sbot"] != nil { + conf.GoSbot.presence = presence["go-sbot"].(map[string]interface{}) + } else { + level.Warn(log).Log("event", "read config", "msg", "no [go-sbot] detected in config file - I am not reading anything from the config file", "path", configPath) + conf.GoSbot.presence = make(map[string]interface{}) + } + + // help repo path's default to align with common user expectations + conf.GoSbot.Repo = expandPath(conf.GoSbot.Repo) + + return conf.GoSbot, true +} + +func ReadConfigSbotCli(configPath string) (SbotCliConfig, bool) { + var conf MergedConfig + + // setup logger if not yet setup (used for tests) + log := testutils.NewRelativeTimeLogger(nil) + + data, err := os.ReadFile(configPath) + if err != nil { + level.Info(log).Log("event", "read config", "msg", "no config detected", "path", configPath) + return conf.SbotCli, false + } + + level.Info(log).Log("event", "read config", "msg", "config detected", "path", configPath) + + // 1) first we unmarshal into struct for type checks + decoder := json.NewDecoder(toml.New(bytes.NewBuffer(data))) + err = decoder.Decode(&conf) + check(err, "decode into struct") + + // 2) then we unmarshal into a map for presence check (to make sure bools are treated correctly) + presence := make(map[string]interface{}) + decoder = json.NewDecoder(toml.New(bytes.NewBuffer(data))) + err = decoder.Decode(&presence) + check(err, "decode into presence map") + if presence["sbotcli"] != nil { + conf.SbotCli.presence = presence["sbotcli"].(map[string]interface{}) + } else { + level.Warn(log).Log("event", "read config", "msg", "no [sbotcli] detected in config file - I am not reading anything from the config file", "path", configPath) + conf.SbotCli.presence = make(map[string]interface{}) + } + + return conf.SbotCli, true +} + +// ensure the following type of path expansions take place: +// * ~/.ssb => /home//.ssb +// * .ssb => /home//.ssb +// * /stuff/.ssb => /stuff/.ssb +func expandPath(p string) string { + home, err := os.UserHomeDir() + if err != nil { + loglib.Fatalln("could not get user home directory (os.UserHomeDir()") + } + + if strings.HasPrefix(p, "~") { + p = strings.Replace(p, "~", home, 1) + } + + // not relative path, not absolute path => + // place relative to home dir "~/" + if !filepath.IsAbs(p) { + p = filepath.Join(home, p) + } + + return p +} + +func (booly ConfigBool) MarshalJSON() ([]byte, error) { + temp := (bool)(booly) + b, err := json.Marshal(temp) + return b, err +} + +func (booly *ConfigBool) UnmarshalJSON(b []byte) error { + // unmarshal into interface{} first, as a bool can't be unmarshaled into a string + var v interface{} + err := json.Unmarshal(b, &v) + if err != nil { + return eout(err, "unmarshal config bool") + } + + // go through a type assertion dance, capturing the two cases: + // 1. if the config value is a proper boolean, and + // 2. if the config value is a boolish string (e.g. "true" or "1") + var temp bool + if val, ok := v.(bool); ok { + temp = val + } else if s, ok := v.(string); ok { + temp = booleanIsTrue(s) + if !temp { + // catch strings that cause a false value, but which aren't boolish + if s != "false" && s != "0" && s != "no" && s != "off" { + return errors.New("non-boolean string found when unmarshaling boolish values") + } + } + } + *booly = (ConfigBool)(temp) + + return nil +} + +func booleanIsTrue(s string) bool { + return s == "true" || s == "1" || s == "yes" || s == "on" +} + +func readEnvironmentBoolean(s string) ConfigBool { + var booly ConfigBool + err := json.Unmarshal([]byte(s), booly) + check(err, "parsing environment variable bool") + return booly +} + +func eout(err error, msg string, args ...interface{}) error { + if err != nil { + msg = fmt.Sprintf(msg, args...) + return fmt.Errorf("%s (%w)", msg, err) + } + return nil +} + +func check(err error, msg string, args ...interface{}) { + if err = eout(err, msg, args...); err != nil { + loglib.Fatalln(err) + } +}