github.com/Synthesix/Sia@v1.3.3-0.20180413141344-f863baeed3ca/cmd/siad/daemon.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"os/signal"
     8  	"strconv"
     9  	"strings"
    10  	"syscall"
    11  	"time"
    12  
    13  	"github.com/Synthesix/Sia/build"
    14  	"github.com/Synthesix/Sia/crypto"
    15  	"github.com/Synthesix/Sia/modules"
    16  	"github.com/Synthesix/Sia/profile"
    17  	mnemonics "github.com/NebulousLabs/entropy-mnemonics"
    18  
    19  	"github.com/spf13/cobra"
    20  	"golang.org/x/crypto/ssh/terminal"
    21  )
    22  
    23  // passwordPrompt securely reads a password from stdin.
    24  func passwordPrompt(prompt string) (string, error) {
    25  	fmt.Print(prompt)
    26  	pw, err := terminal.ReadPassword(int(syscall.Stdin))
    27  	fmt.Println()
    28  	return string(pw), err
    29  }
    30  
    31  // verifyAPISecurity checks that the security values are consistent with a
    32  // sane, secure system.
    33  func verifyAPISecurity(config Config) error {
    34  	// Make sure that only the loopback address is allowed unless the
    35  	// --disable-api-security flag has been used.
    36  	if !config.Siad.AllowAPIBind {
    37  		addr := modules.NetAddress(config.Siad.APIaddr)
    38  		if !addr.IsLoopback() {
    39  			if addr.Host() == "" {
    40  				return fmt.Errorf("a blank host will listen on all interfaces, did you mean localhost:%v?\nyou must pass --disable-api-security to bind Siad to a non-localhost address", addr.Port())
    41  			}
    42  			return errors.New("you must pass --disable-api-security to bind Siad to a non-localhost address")
    43  		}
    44  		return nil
    45  	}
    46  
    47  	// If the --disable-api-security flag is used, enforce that
    48  	// --authenticate-api must also be used.
    49  	if config.Siad.AllowAPIBind && !config.Siad.AuthenticateAPI {
    50  		return errors.New("cannot use --disable-api-security without setting an api password")
    51  	}
    52  	return nil
    53  }
    54  
    55  // processNetAddr adds a ':' to a bare integer, so that it is a proper port
    56  // number.
    57  func processNetAddr(addr string) string {
    58  	_, err := strconv.Atoi(addr)
    59  	if err == nil {
    60  		return ":" + addr
    61  	}
    62  	return addr
    63  }
    64  
    65  // processModules makes the modules string lowercase to make checking if a
    66  // module in the string easier, and returns an error if the string contains an
    67  // invalid module character.
    68  func processModules(modules string) (string, error) {
    69  	modules = strings.ToLower(modules)
    70  	validModules := "cghmrtwe"
    71  	invalidModules := modules
    72  	for _, m := range validModules {
    73  		invalidModules = strings.Replace(invalidModules, string(m), "", 1)
    74  	}
    75  	if len(invalidModules) > 0 {
    76  		return "", errors.New("Unable to parse --modules flag, unrecognized or duplicate modules: " + invalidModules)
    77  	}
    78  	return modules, nil
    79  }
    80  
    81  // processProfileFlags checks that the flags given for profiling are valid.
    82  func processProfileFlags(profile string) (string, error) {
    83  	profile = strings.ToLower(profile)
    84  	validProfiles := "cmt"
    85  
    86  	invalidProfiles := profile
    87  	for _, p := range validProfiles {
    88  		invalidProfiles = strings.Replace(invalidProfiles, string(p), "", 1)
    89  	}
    90  	if len(invalidProfiles) > 0 {
    91  		return "", errors.New("Unable to parse --profile flags, unrecognized or duplicate flags: " + invalidProfiles)
    92  	}
    93  	return profile, nil
    94  }
    95  
    96  // processConfig checks the configuration values and performs cleanup on
    97  // incorrect-but-allowed values.
    98  func processConfig(config Config) (Config, error) {
    99  	var err1, err2 error
   100  	config.Siad.APIaddr = processNetAddr(config.Siad.APIaddr)
   101  	config.Siad.RPCaddr = processNetAddr(config.Siad.RPCaddr)
   102  	config.Siad.HostAddr = processNetAddr(config.Siad.HostAddr)
   103  	config.Siad.Modules, err1 = processModules(config.Siad.Modules)
   104  	config.Siad.Profile, err2 = processProfileFlags(config.Siad.Profile)
   105  	err3 := verifyAPISecurity(config)
   106  	err := build.JoinErrors([]error{err1, err2, err3}, ", and ")
   107  	if err != nil {
   108  		return Config{}, err
   109  	}
   110  	return config, nil
   111  }
   112  
   113  // unlockWallet is called on siad startup and attempts to automatically
   114  // unlock the wallet with the given password string.
   115  func unlockWallet(w modules.Wallet, password string) error {
   116  	var validKeys []crypto.TwofishKey
   117  	dicts := []mnemonics.DictionaryID{"english", "german", "japanese"}
   118  	for _, dict := range dicts {
   119  		seed, err := modules.StringToSeed(password, dict)
   120  		if err != nil {
   121  			continue
   122  		}
   123  		validKeys = append(validKeys, crypto.TwofishKey(crypto.HashObject(seed)))
   124  	}
   125  	validKeys = append(validKeys, crypto.TwofishKey(crypto.HashObject(password)))
   126  	for _, key := range validKeys {
   127  		if err := w.Unlock(key); err == nil {
   128  			return nil
   129  		}
   130  	}
   131  	return modules.ErrBadEncryptionKey
   132  }
   133  
   134  // startDaemon uses the config parameters to initialize Sia modules and start
   135  // siad.
   136  func startDaemon(config Config) (err error) {
   137  	if config.Siad.AuthenticateAPI {
   138  		password := os.Getenv("SIA_API_PASSWORD")
   139  		if password != "" {
   140  			fmt.Println("Using SIA_API_PASSWORD environment variable")
   141  			config.APIPassword = password
   142  		} else {
   143  			// Prompt user for API password.
   144  			config.APIPassword, err = passwordPrompt("Enter API password: ")
   145  			if err != nil {
   146  				return err
   147  			}
   148  			if config.APIPassword == "" {
   149  				return errors.New("password cannot be blank")
   150  			}
   151  		}
   152  	}
   153  
   154  	// Print the siad Version and GitRevision
   155  	fmt.Println("Sia Daemon v" + build.Version)
   156  	if build.GitRevision == "" {
   157  		fmt.Println("WARN: compiled without build commit or version. To compile correctly, please use the makefile")
   158  	} else {
   159  		fmt.Println("Git Revision " + build.GitRevision)
   160  	}
   161  
   162  	// Install a signal handler that will catch exceptions thrown by mmap'd
   163  	// files.
   164  	// NOTE: ideally we would catch SIGSEGV here too, since that signal can
   165  	// also be thrown by an mmap I/O error. However, SIGSEGV can occur under
   166  	// other circumstances as well, and in those cases, we will want a full
   167  	// stack trace.
   168  	mmapChan := make(chan os.Signal, 1)
   169  	signal.Notify(mmapChan, syscall.SIGBUS)
   170  	go func() {
   171  		<-mmapChan
   172  		fmt.Println("A fatal I/O exception (SIGBUS) has occurred.")
   173  		fmt.Println("Please check your disk for errors.")
   174  		os.Exit(1)
   175  	}()
   176  
   177  	// Print a startup message.
   178  	fmt.Println("Loading...")
   179  	loadStart := time.Now()
   180  	srv, err := NewServer(config)
   181  	if err != nil {
   182  		return err
   183  	}
   184  	errChan := make(chan error)
   185  	go func() {
   186  		errChan <- srv.Serve()
   187  	}()
   188  	err = srv.loadModules()
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	// listen for kill signals
   194  	sigChan := make(chan os.Signal, 1)
   195  	signal.Notify(sigChan, os.Interrupt, os.Kill)
   196  
   197  	// Print a 'startup complete' message.
   198  	startupTime := time.Since(loadStart)
   199  	fmt.Println("Finished loading in", startupTime.Seconds(), "seconds")
   200  
   201  	// wait for Serve to return or for kill signal to be caught
   202  	err = func() error {
   203  		select {
   204  		case err := <-errChan:
   205  			return err
   206  		case <-sigChan:
   207  			fmt.Println("\rCaught stop signal, quitting...")
   208  			return srv.Close()
   209  		}
   210  	}()
   211  	if err != nil {
   212  		build.Critical(err)
   213  	}
   214  
   215  	return nil
   216  }
   217  
   218  // startDaemonCmd is a passthrough function for startDaemon.
   219  func startDaemonCmd(cmd *cobra.Command, _ []string) {
   220  	var profileCPU, profileMem, profileTrace bool
   221  
   222  	profileCPU = strings.Contains(globalConfig.Siad.Profile, "c")
   223  	profileMem = strings.Contains(globalConfig.Siad.Profile, "m")
   224  	profileTrace = strings.Contains(globalConfig.Siad.Profile, "t")
   225  
   226  	if build.DEBUG {
   227  		profileCPU = true
   228  		profileMem = true
   229  	}
   230  
   231  	if profileCPU || profileMem || profileTrace {
   232  		go profile.StartContinuousProfile(globalConfig.Siad.ProfileDir, profileCPU, profileMem, profileTrace)
   233  	}
   234  
   235  	// Start siad. startDaemon will only return when it is shutting down.
   236  	err := startDaemon(globalConfig)
   237  	if err != nil {
   238  		die(err)
   239  	}
   240  
   241  	// Daemon seems to have closed cleanly. Print a 'closed' mesasge.
   242  	fmt.Println("Shutdown complete.")
   243  }