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 }