diff --git a/cmd/btcctl/config.go b/cmd/btcctl/config.go index a6c37f82..339b2ed5 100644 --- a/cmd/btcctl/config.go +++ b/cmd/btcctl/config.go @@ -6,9 +6,11 @@ package main import ( "fmt" + "io/ioutil" "net" "os" "path/filepath" + "regexp" "strings" "github.com/btcsuite/btcd/btcjson" @@ -215,6 +217,13 @@ func loadConfig() (*config, []string, error) { os.Exit(0) } + if _, err := os.Stat(preCfg.ConfigFile); os.IsNotExist(err) { + err := createDefaultConfigFile(preCfg.ConfigFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating a default config file: %v\n", err) + } + } + // Load additional config from file. parser := flags.NewParser(&cfg, flags.Default) err = flags.NewIniParser(parser).ParseFile(preCfg.ConfigFile) @@ -268,3 +277,56 @@ func loadConfig() (*config, []string, error) { return &cfg, remainingArgs, nil } + +// createDefaultConfig creates a basic config file at the given destination path. +// For this it tries to read the btcd config file at its default path, and extract +// the RPC user and password from it. +func createDefaultConfigFile(destinationPath string) error { + // Create the destination directory if it does not exists + os.MkdirAll(filepath.Dir(destinationPath), 0700) + + // Read btcd.conf from its default path + btcdConfigPath := filepath.Join(btcdHomeDir, "btcd.conf") + btcdConfigFile, err := os.Open(btcdConfigPath) + if err != nil { + return err + } + defer btcdConfigFile.Close() + content, err := ioutil.ReadAll(btcdConfigFile) + if err != nil { + return err + } + + // Extract the rpcuser + rpcUserRegexp, err := regexp.Compile(`(?m)^\s*rpcuser=([^\s]+)`) + if err != nil { + return err + } + userSubmatches := rpcUserRegexp.FindSubmatch(content) + if userSubmatches == nil { + // No user found, nothing to do + return nil + } + + // Extract the rpcpass + rpcPassRegexp, err := regexp.Compile(`(?m)^\s*rpcpass=([^\s]+)`) + if err != nil { + return err + } + passSubmatches := rpcPassRegexp.FindSubmatch(content) + if passSubmatches == nil { + // No password found, nothing to do + return nil + } + + // Create the destination file and write the rpcuser and rpcpass to it + dest, err := os.OpenFile(destinationPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0700) + if err != nil { + return err + } + defer dest.Close() + + dest.WriteString(fmt.Sprintf("rpcuser=%s\nrpcpass=%s", string(userSubmatches[1]), string(passSubmatches[1]))) + + return nil +} diff --git a/config.go b/config.go index 0e00caa0..a6ee71a0 100644 --- a/config.go +++ b/config.go @@ -5,8 +5,12 @@ package main import ( + "bufio" + "crypto/rand" + "encoding/base64" "errors" "fmt" + "io" "net" "os" "path/filepath" @@ -395,6 +399,13 @@ func loadConfig() (*config, []string, error) { if !(preCfg.RegressionTest || preCfg.SimNet) || preCfg.ConfigFile != defaultConfigFile { + if _, err := os.Stat(preCfg.ConfigFile); os.IsNotExist(err) { + err := createDefaultConfigFile(preCfg.ConfigFile) + if err != nil { + btcdLog.Warnf("Error creating a default config file: %v", err) + } + } + err := flags.NewIniParser(parser).ParseFile(preCfg.ConfigFile) if err != nil { if _, ok := err.(*os.PathError); !ok { @@ -880,6 +891,66 @@ func loadConfig() (*config, []string, error) { return &cfg, remainingArgs, nil } +// createDefaultConfig copies the file sample-btcd.conf to the given destination path, +// and populates it with some randomly generated RPC username and password. +func createDefaultConfigFile(destinationPath string) error { + // Create the destination directory if it does not exists + os.MkdirAll(filepath.Dir(destinationPath), 0700) + + // We get the sample config file path, which is in the same directory as this file. + _, path, _, _ := runtime.Caller(0) + sampleConfigPath := filepath.Join(filepath.Dir(path), "sample-btcd.conf") + + // We generate a random user and password + randomBytes := make([]byte, 20) + _, err := rand.Read(randomBytes) + if err != nil { + return err + } + generatedRPCUser := base64.StdEncoding.EncodeToString(randomBytes) + + _, err = rand.Read(randomBytes) + if err != nil { + return err + } + generatedRPCPass := base64.StdEncoding.EncodeToString(randomBytes) + + src, err := os.Open(sampleConfigPath) + if err != nil { + return err + } + defer src.Close() + + dest, err := os.OpenFile(destinationPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0700) + if err != nil { + return err + } + defer dest.Close() + + // We copy every line from the sample config file to the destination, + // only replacing the two lines for rpcuser and rpcpass + reader := bufio.NewReader(src) + for err != io.EOF { + var line string + line, err = reader.ReadString('\n') + if err != nil && err != io.EOF { + return err + } + + if strings.Contains(line, "rpcuser=") { + line = "rpcuser=" + string(generatedRPCUser) + "\n" + } else if strings.Contains(line, "rpcpass=") { + line = "rpcpass=" + string(generatedRPCPass) + "\n" + } + + if _, err := dest.WriteString(line); err != nil { + return err + } + } + + return nil +} + // btcdDial connects to the address on the named network using the appropriate // dial function depending on the address and configuration options. For // example, .onion addresses will be dialed using the onion specific proxy if diff --git a/config_test.go b/config_test.go new file mode 100644 index 00000000..4083c544 --- /dev/null +++ b/config_test.go @@ -0,0 +1,47 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + "regexp" + "testing" +) + +var ( + rpcuserRegexp = regexp.MustCompile("(?m)^rpcuser=.+$") + rpcpassRegexp = regexp.MustCompile("(?m)^rpcpass=.+$") +) + +func TestCreateDefaultConfigFile(t *testing.T) { + // Setup a temporary directory + tmpDir, err := ioutil.TempDir("", "btcd") + if err != nil { + t.Fatalf("Failed creating a temporary directory: %v", err) + } + testpath := filepath.Join(tmpDir, "test.conf") + // Clean-up + defer func() { + os.Remove(testpath) + os.Remove(tmpDir) + }() + + err = createDefaultConfigFile(testpath) + + if err != nil { + t.Fatalf("Failed to create a default config file: %v", err) + } + + content, err := ioutil.ReadFile(testpath) + if err != nil { + t.Fatalf("Failed to read generated default config file: %v", err) + } + + if !rpcuserRegexp.Match(content) { + t.Error("Could not find rpcuser in generated default config file.") + } + + if !rpcpassRegexp.Match(content) { + t.Error("Could not find rpcpass in generated default config file.") + } +}