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

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"os"
    10  	"reflect"
    11  
    12  	"github.com/spf13/cobra"
    13  
    14  	"github.com/Synthesix/Sia/build"
    15  	"github.com/Synthesix/Sia/node/api"
    16  )
    17  
    18  var (
    19  	// Flags.
    20  	addr              string // override default API address
    21  	hostVerbose       bool   // display additional host info
    22  	initForce         bool   // destroy and reencrypt the wallet on init if it already exists
    23  	initPassword      bool   // supply a custom password when creating a wallet
    24  	renterListVerbose bool   // Show additional info about uploaded files.
    25  	renterShowHistory bool   // Show download history in addition to download queue.
    26  )
    27  
    28  var (
    29  	// Globals.
    30  	rootCmd *cobra.Command // Root command cobra object, used by bash completion cmd.
    31  )
    32  
    33  var (
    34  	// User-supplied password, cached so that we don't need to prompt multiple
    35  	// times.
    36  	apiPassword string
    37  )
    38  
    39  // Exit codes.
    40  // inspired by sysexits.h
    41  const (
    42  	exitCodeGeneral = 1  // Not in sysexits.h, but is standard practice.
    43  	exitCodeUsage   = 64 // EX_USAGE in sysexits.h
    44  )
    45  
    46  // non2xx returns true for non-success HTTP status codes.
    47  func non2xx(code int) bool {
    48  	return code < 200 || code > 299
    49  }
    50  
    51  // decodeError returns the api.Error from a API response. This method should
    52  // only be called if the response's status code is non-2xx. The error returned
    53  // may not be of type api.Error in the event of an error unmarshalling the
    54  // JSON.
    55  func decodeError(resp *http.Response) error {
    56  	var apiErr api.Error
    57  	err := json.NewDecoder(resp.Body).Decode(&apiErr)
    58  	if err != nil {
    59  		return err
    60  	}
    61  	return apiErr
    62  }
    63  
    64  // apiGet wraps a GET request with a status code check, such that if the GET does
    65  // not return 2xx, the error will be read and returned. The response body is
    66  // not closed.
    67  func apiGet(call string) (*http.Response, error) {
    68  	if host, port, _ := net.SplitHostPort(addr); host == "" {
    69  		addr = net.JoinHostPort("localhost", port)
    70  	}
    71  	resp, err := api.HttpGET("http://" + addr + call)
    72  	if err != nil {
    73  		return nil, errors.New("no response from daemon")
    74  	}
    75  	// check error code
    76  	if resp.StatusCode == http.StatusUnauthorized {
    77  		// retry request with authentication.
    78  		resp.Body.Close()
    79  		if apiPassword == "" {
    80  			apiPassword = os.Getenv("SIA_API_PASSWORD")
    81  			if apiPassword != "" {
    82  				fmt.Println("Using SIA_API_PASSWORD environment variable")
    83  			} else {
    84  				// prompt for password and store it in a global var for subsequent
    85  				// calls
    86  				apiPassword, err = passwordPrompt("API password: ")
    87  				if err != nil {
    88  					return nil, err
    89  				}
    90  			}
    91  		}
    92  		resp, err = api.HttpGETAuthenticated("http://"+addr+call, apiPassword)
    93  		if err != nil {
    94  			return nil, errors.New("no response from daemon - authentication failed")
    95  		}
    96  	}
    97  	if resp.StatusCode == http.StatusNotFound {
    98  		resp.Body.Close()
    99  		return nil, errors.New("API call not recognized: " + call)
   100  	}
   101  	if non2xx(resp.StatusCode) {
   102  		err := decodeError(resp)
   103  		resp.Body.Close()
   104  		return nil, err
   105  	}
   106  	return resp, nil
   107  }
   108  
   109  // getAPI makes a GET API call and decodes the response. An error is returned
   110  // if the response status is not 2xx.
   111  func getAPI(call string, obj interface{}) error {
   112  	resp, err := apiGet(call)
   113  	if err != nil {
   114  		return err
   115  	}
   116  	defer resp.Body.Close()
   117  
   118  	if resp.StatusCode == http.StatusNoContent {
   119  		return errors.New("expecting a response, but API returned status code 204 No Content")
   120  	}
   121  
   122  	err = json.NewDecoder(resp.Body).Decode(obj)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	return nil
   127  }
   128  
   129  // get makes an API call and discards the response. An error is returned if the
   130  // response status is not 2xx.
   131  func get(call string) error {
   132  	resp, err := apiGet(call)
   133  	if err != nil {
   134  		return err
   135  	}
   136  	resp.Body.Close()
   137  	return nil
   138  }
   139  
   140  // apiPost wraps a POST request with a status code check, such that if the POST
   141  // does not return 2xx, the error will be read and returned. The response body
   142  // is not closed.
   143  func apiPost(call, vals string) (*http.Response, error) {
   144  	if host, port, _ := net.SplitHostPort(addr); host == "" {
   145  		addr = net.JoinHostPort("localhost", port)
   146  	}
   147  
   148  	resp, err := api.HttpPOST("http://"+addr+call, vals)
   149  	if err != nil {
   150  		return nil, errors.New("no response from daemon")
   151  	}
   152  	// check error code
   153  	if resp.StatusCode == http.StatusUnauthorized {
   154  		resp.Body.Close()
   155  		apiPassword = os.Getenv("SIA_API_PASSWORD")
   156  		if apiPassword != "" {
   157  			fmt.Println("Using SIA_API_PASSWORD environment variable")
   158  		} else {
   159  			// Prompt for password and retry request with authentication.
   160  			apiPassword, err = passwordPrompt("API password: ")
   161  			if err != nil {
   162  				return nil, err
   163  			}
   164  		}
   165  		resp, err = api.HttpPOSTAuthenticated("http://"+addr+call, vals, apiPassword)
   166  		if err != nil {
   167  			return nil, errors.New("no response from daemon - authentication failed")
   168  		}
   169  	}
   170  	if resp.StatusCode == http.StatusNotFound {
   171  		resp.Body.Close()
   172  		return nil, errors.New("API call not recognized: " + call)
   173  	}
   174  	if non2xx(resp.StatusCode) {
   175  		err := decodeError(resp)
   176  		resp.Body.Close()
   177  		return nil, err
   178  	}
   179  	return resp, nil
   180  }
   181  
   182  // postResp makes a POST API call and decodes the response. An error is
   183  // returned if the response status is not 2xx.
   184  func postResp(call, vals string, obj interface{}) error {
   185  	resp, err := apiPost(call, vals)
   186  	if err != nil {
   187  		return err
   188  	}
   189  	defer resp.Body.Close()
   190  
   191  	if resp.StatusCode == http.StatusNoContent {
   192  		return errors.New("expecting a response, but API returned status code 204 No Content")
   193  	}
   194  
   195  	err = json.NewDecoder(resp.Body).Decode(obj)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	return nil
   200  }
   201  
   202  // post makes an API call and discards the response. An error is returned if
   203  // the response status is not 2xx.
   204  func post(call, vals string) error {
   205  	resp, err := apiPost(call, vals)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	resp.Body.Close()
   210  	return nil
   211  }
   212  
   213  // wrap wraps a generic command with a check that the command has been
   214  // passed the correct number of arguments. The command must take only strings
   215  // as arguments.
   216  func wrap(fn interface{}) func(*cobra.Command, []string) {
   217  	fnVal, fnType := reflect.ValueOf(fn), reflect.TypeOf(fn)
   218  	if fnType.Kind() != reflect.Func {
   219  		panic("wrapped function has wrong type signature")
   220  	}
   221  	for i := 0; i < fnType.NumIn(); i++ {
   222  		if fnType.In(i).Kind() != reflect.String {
   223  			panic("wrapped function has wrong type signature")
   224  		}
   225  	}
   226  
   227  	return func(cmd *cobra.Command, args []string) {
   228  		if len(args) != fnType.NumIn() {
   229  			cmd.UsageFunc()(cmd)
   230  			os.Exit(exitCodeUsage)
   231  		}
   232  		argVals := make([]reflect.Value, fnType.NumIn())
   233  		for i := range args {
   234  			argVals[i] = reflect.ValueOf(args[i])
   235  		}
   236  		fnVal.Call(argVals)
   237  	}
   238  }
   239  
   240  // die prints its arguments to stderr, then exits the program with the default
   241  // error code.
   242  func die(args ...interface{}) {
   243  	fmt.Fprintln(os.Stderr, args...)
   244  	os.Exit(exitCodeGeneral)
   245  }
   246  
   247  func main() {
   248  	root := &cobra.Command{
   249  		Use:   os.Args[0],
   250  		Short: "Sia Client v" + build.Version,
   251  		Long:  "Sia Client v" + build.Version,
   252  		Run:   wrap(consensuscmd),
   253  	}
   254  
   255  	rootCmd = root
   256  
   257  	// create command tree
   258  	root.AddCommand(versionCmd)
   259  	root.AddCommand(stopCmd)
   260  
   261  	root.AddCommand(updateCmd)
   262  	updateCmd.AddCommand(updateCheckCmd)
   263  
   264  	root.AddCommand(hostCmd)
   265  	hostCmd.AddCommand(hostConfigCmd, hostAnnounceCmd, hostFolderCmd, hostSectorCmd)
   266  	hostFolderCmd.AddCommand(hostFolderAddCmd, hostFolderRemoveCmd, hostFolderResizeCmd)
   267  	hostSectorCmd.AddCommand(hostSectorDeleteCmd)
   268  	hostCmd.Flags().BoolVarP(&hostVerbose, "verbose", "v", false, "Display detailed host info")
   269  
   270  	root.AddCommand(hostdbCmd)
   271  	hostdbCmd.AddCommand(hostdbViewCmd)
   272  	hostdbCmd.Flags().IntVarP(&hostdbNumHosts, "numhosts", "n", 0, "Number of hosts to display from the hostdb")
   273  	hostdbCmd.Flags().BoolVarP(&hostdbVerbose, "verbose", "v", false, "Display full hostdb information")
   274  
   275  	root.AddCommand(minerCmd)
   276  	minerCmd.AddCommand(minerStartCmd, minerStopCmd)
   277  
   278  	root.AddCommand(walletCmd)
   279  	walletCmd.AddCommand(walletAddressCmd, walletAddressesCmd, walletChangepasswordCmd, walletInitCmd, walletInitSeedCmd,
   280  		walletLoadCmd, walletLockCmd, walletSeedsCmd, walletSendCmd, walletSweepCmd,
   281  		walletBalanceCmd, walletTransactionsCmd, walletUnlockCmd)
   282  	walletInitCmd.Flags().BoolVarP(&initPassword, "password", "p", false, "Prompt for a custom password")
   283  	walletInitCmd.Flags().BoolVarP(&initForce, "force", "", false, "destroy the existing wallet and re-encrypt")
   284  	walletInitSeedCmd.Flags().BoolVarP(&initForce, "force", "", false, "destroy the existing wallet")
   285  	walletLoadCmd.AddCommand(walletLoad033xCmd, walletLoadSeedCmd, walletLoadSiagCmd)
   286  	walletSendCmd.AddCommand(walletSendSiacoinsCmd, walletSendSiafundsCmd)
   287  	walletUnlockCmd.Flags().BoolVarP(&initPassword, "password", "p", false, "Display interactive password prompt even if SIA_WALLET_PASSWORD is set")
   288  
   289  	root.AddCommand(renterCmd)
   290  	renterCmd.AddCommand(renterFilesDeleteCmd, renterFilesDownloadCmd,
   291  		renterDownloadsCmd, renterAllowanceCmd, renterSetAllowanceCmd,
   292  		renterContractsCmd, renterFilesListCmd, renterFilesRenameCmd,
   293  		renterFilesUploadCmd, renterUploadsCmd, renterExportCmd,
   294  		renterPricesCmd)
   295  
   296  	renterContractsCmd.AddCommand(renterContractsViewCmd)
   297  	renterAllowanceCmd.AddCommand(renterAllowanceCancelCmd)
   298  
   299  	renterCmd.Flags().BoolVarP(&renterListVerbose, "verbose", "v", false, "Show additional file info such as redundancy")
   300  	renterDownloadsCmd.Flags().BoolVarP(&renterShowHistory, "history", "H", false, "Show download history in addition to the download queue")
   301  	renterFilesListCmd.Flags().BoolVarP(&renterListVerbose, "verbose", "v", false, "Show additional file info such as redundancy")
   302  	renterExportCmd.AddCommand(renterExportContractTxnsCmd)
   303  
   304  	root.AddCommand(gatewayCmd)
   305  	gatewayCmd.AddCommand(gatewayConnectCmd, gatewayDisconnectCmd, gatewayAddressCmd, gatewayListCmd)
   306  
   307  	root.AddCommand(consensusCmd)
   308  
   309  	root.AddCommand(bashcomplCmd)
   310  	root.AddCommand(mangenCmd)
   311  
   312  	// parse flags
   313  	root.PersistentFlags().StringVarP(&addr, "addr", "a", "localhost:9980", "which host/port to communicate with (i.e. the host/port siad is listening on)")
   314  
   315  	// run
   316  	if err := root.Execute(); err != nil {
   317  		// Since no commands return errors (all commands set Command.Run instead of
   318  		// Command.RunE), Command.Execute() should only return an error on an
   319  		// invalid command or flag. Therefore Command.Usage() was called (assuming
   320  		// Command.SilenceUsage is false) and we should exit with exitCodeUsage.
   321  		os.Exit(exitCodeUsage)
   322  	}
   323  }