github.com/decred/politeia@v1.4.0/politeiawww/cmd/cmswww/cmswww.go (about) 1 // Copyright (c) 2017-2020 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "bufio" 9 "encoding/csv" 10 "encoding/hex" 11 "errors" 12 "fmt" 13 "net/url" 14 "os" 15 "strconv" 16 "strings" 17 18 "github.com/decred/dcrd/dcrutil/v3" 19 "github.com/decred/politeia/politeiad/api/v1/identity" 20 cms "github.com/decred/politeia/politeiawww/api/cms/v1" 21 pi "github.com/decred/politeia/politeiawww/api/www/v1" 22 "github.com/decred/politeia/politeiawww/cmd/shared" 23 "github.com/decred/politeia/util" 24 flags "github.com/jessevdk/go-flags" 25 ) 26 27 const ( 28 // Config settings 29 defaultHomeDirname = "cmswww" 30 defaultDataDirname = "data" 31 defaultConfigFilename = "cmswww.conf" 32 ) 33 34 var ( 35 // Global variables for cmswww commands 36 cfg *shared.Config 37 client *shared.Client 38 39 // Config settings 40 defaultHomeDir = dcrutil.AppDataDir(defaultHomeDirname, false) 41 42 // errInvoiceCSVNotFound is emitted when a invoice csv file is 43 // required but has not been passed into the command. 44 errInvoiceCSVNotFound = errors.New("invoice csv file not found. " + 45 "You must either provide a csv file or use the --random flag.") 46 47 // errInvalidDCCType is emitted if there is a bad dcc type used. 48 errInvalidDCCType = errors.New("submitted dcc type is invalid," + 49 "must be 1 (Issuance) or 2 (Revocation)") 50 ) 51 52 type cmswww struct { 53 // XXX the config does not need to be a part of this struct, but 54 // is included so that the config cli flags print as part of the 55 // cmswww help message. This is handled by go-flags. 56 Config shared.Config 57 58 // Commands 59 ActiveVotes ActiveVotesCmd `command:"activevotes" description:"(user) get the dccs that are being voted on"` 60 BatchProposals BatchProposalsCmd `command:"batchproposals" description:"(user) retrieve a set of proposals"` 61 NewComment NewCommentCmd `command:"newcomment" description:"(user) create a new comment"` 62 CensorComment CensorCommentCmd `command:"censorcomment" description:"(admin) censor a comment"` 63 ChangePassword shared.UserPasswordChangeCmd `command:"changepassword" description:"(user) change the password for the logged in user"` 64 ChangeUsername shared.UserUsernameChangeCmd `command:"changeusername" description:"(user) change the username for the logged in user"` 65 CMSUsers CMSUsersCmd `command:"cmsusers" description:"(user) get a list of cms users"` 66 CodeStats CodeStatsCmd `command:"codestats" description:"(user) get a list of code stats per repo for the given userid"` 67 DCCComments DCCCommentsCmd `command:"dcccomments" description:"(user) get the comments for a dcc proposal"` 68 DCCDetails DCCDetailsCmd `command:"dccdetails" description:"(user) get the details of a dcc"` 69 EditInvoice EditInvoiceCmd `command:"editinvoice" description:"(user) edit a invoice"` 70 EditUser EditUserCmd `command:"edituser" description:"(user) edit current cms user information"` 71 GeneratePayouts GeneratePayoutsCmd `command:"generatepayouts" description:"(admin) generate a list of payouts with addresses and amounts to pay"` 72 GetDCCs GetDCCsCmd `command:"getdccs" description:"(user) get all dccs (optional by status)"` 73 Help HelpCmd `command:"help" description:" print a detailed help message for a specific command"` 74 InvoiceComments InvoiceCommentsCmd `command:"invoicecomments" description:"(user) get the comments for a invoice"` 75 InvoiceExchangeRate InvoiceExchangeRateCmd `command:"invoiceexchangerate" description:"(user) get exchange rate for a given month/year"` 76 InviteNewUser InviteNewUserCmd `command:"invite" description:"(admin) invite a new user"` 77 InvoiceDetails InvoiceDetailsCmd `command:"invoicedetails" description:"(public) get the details of a proposal"` 78 InvoicePayouts InvoicePayoutsCmd `command:"invoicepayouts" description:"(admin) generate paid invoice list for a given date range"` 79 Invoices InvoicesCmd `command:"invoices" description:"(user) get all invoices (optional with optional parameters)"` 80 Login shared.LoginCmd `command:"login" description:"(public) login to Politeia"` 81 Logout shared.LogoutCmd `command:"logout" description:"(public) logout of Politeia"` 82 CMSManageUser CMSManageUserCmd `command:"cmsmanageuser" description:"(admin) edit certain properties of the specified user"` 83 ManageUser shared.UserManageCmd `command:"manageuser" description:"(admin) edit certain properties of the specified user"` 84 Me shared.MeCmd `command:"me" description:"(user) get user details for the logged in user"` 85 NewDCC NewDCCCmd `command:"newdcc" description:"(user) creates a new dcc proposal"` 86 NewDCCComment NewDCCCommentCmd `command:"newdcccomment" description:"(user) creates a new comment on a dcc proposal"` 87 NewInvoice NewInvoiceCmd `command:"newinvoice" description:"(user) create a new invoice"` 88 PayInvoices PayInvoicesCmd `command:"payinvoices" description:"(admin) set all approved invoices to paid"` 89 Policy PolicyCmd `command:"policy" description:"(public) get the server policy"` 90 ProposalOwner ProposalOwnerCmd `command:"proposalowner" description:"(user) get owners of a proposal"` 91 ProposalBilling ProposalBillingCmd `command:"proposalbilling" description:"(user) get billing information for a proposal"` 92 ProposalBillingDetails ProposalBillingDetailsCmd `command:"proposalbillingdetails" description:"(admin) get billing information for a proposal"` 93 ProposalBillingSummary ProposalBillingSummaryCmd `command:"proposalbillingsummary" description:"(admin) get all approved proposal billing information"` 94 RegisterUser RegisterUserCmd `command:"register" description:"(public) register an invited user to cms"` 95 ResetPassword shared.UserPasswordResetCmd `command:"resetpassword" description:"(public) reset the password for a user that is not logged in"` 96 SetDCCStatus SetDCCStatusCmd `command:"setdccstatus" description:"(admin) set the status of a DCC"` 97 SetInvoiceStatus SetInvoiceStatusCmd `command:"setinvoicestatus" description:"(admin) set the status of an invoice"` 98 SetTOTP shared.UserTOTPSetCmd `command:"settotp" description:"(user) set the key for TOTP"` 99 StartVote StartVoteCmd `command:"startvote" description:"(admin) start the voting period on a dcc"` 100 SupportOpposeDCC SupportOpposeDCCCmd `command:"supportopposedcc" description:"(user) support or oppose a given DCC"` 101 TestRun TestRunCmd `command:"testrun" description:" test cmswww routes"` 102 TokenInventory shared.TokenInventoryCmd `command:"tokeninventory" description:"(user) get the censorship record tokens of all proposals (passthrough)"` 103 UpdateUserKey shared.UserKeyUpdateCmd `command:"updateuserkey" description:"(user) generate a new identity for the logged in user"` 104 UserDetails UserDetailsCmd `command:"userdetails" description:"(user) get current cms user details"` 105 UserInvoices UserInvoicesCmd `command:"userinvoices" description:"(user) get all invoices submitted by a specific user"` 106 UserSubContractors UserSubContractorsCmd `command:"usersubcontractors" description:"(user) get all users that are linked to the user"` 107 Users shared.UsersCmd `command:"users" description:"(user) get a list of users"` 108 Secret shared.SecretCmd `command:"secret" description:"(user) ping politeiawww"` 109 VerifyTOTP shared.UserTOTPVerifyCmd `command:"verifytotp" description:"(user) verify the set code for TOTP"` 110 Version shared.VersionCmd `command:"version" description:"(public) get server info and CSRF token"` 111 VoteDCC VoteDCCCmd `command:"votedcc" description:"(user) vote for a given DCC during an all contractor vote"` 112 VoteDetails VoteDetailsCmd `command:"votedetails" description:"(user) get the details for a dcc vote"` 113 } 114 115 // signedMerkleRoot calculates the merkle root of the passed in list of files 116 // and metadata, signs the merkle root with the passed in identity and returns 117 // the signature. 118 func signedMerkleRoot(files []pi.File, md []pi.Metadata, id *identity.FullIdentity) (string, error) { 119 if len(files) == 0 { 120 return "", fmt.Errorf("no proposal files found") 121 } 122 mr, err := merkleRoot(files, md) 123 if err != nil { 124 return "", err 125 } 126 sig := id.SignMessage([]byte(mr)) 127 return hex.EncodeToString(sig[:]), nil 128 } 129 130 // verifyInvoice verifies a invoice's merkle root, author signature, and 131 // censorship record. 132 func verifyInvoice(p cms.InvoiceRecord, serverPubKey string) error { 133 if len(p.Files) > 0 { 134 // Verify file digests 135 err := validateDigests(p.Files, nil) 136 if err != nil { 137 return err 138 } 139 // Verify merkle root 140 mr, err := merkleRoot(p.Files, nil) 141 if err != nil { 142 return err 143 } 144 if mr != p.CensorshipRecord.Merkle { 145 return fmt.Errorf("merkle roots do not match") 146 } 147 } 148 149 // Verify invoice signature 150 pid, err := identity.PublicIdentityFromString(p.PublicKey) 151 if err != nil { 152 return err 153 } 154 sig, err := util.ConvertSignature(p.Signature) 155 if err != nil { 156 return err 157 } 158 if !pid.VerifyMessage([]byte(p.CensorshipRecord.Merkle), sig) { 159 return fmt.Errorf("could not verify proposal signature") 160 } 161 162 // Verify censorship record signature 163 id, err := identity.PublicIdentityFromString(serverPubKey) 164 if err != nil { 165 return err 166 } 167 s, err := util.ConvertSignature(p.CensorshipRecord.Signature) 168 if err != nil { 169 return err 170 } 171 msg := []byte(p.CensorshipRecord.Merkle + p.CensorshipRecord.Token) 172 if !id.VerifyMessage(msg, s) { 173 return fmt.Errorf("could not verify censorship record signature") 174 } 175 176 return nil 177 } 178 179 // promptList prompts the user with the given prefix, list of valid responses, 180 // and default list entry to use. The function will repeat the prompt to the 181 // user until they enter a valid response. 182 func promptList(reader *bufio.Reader, prefix string, validResponses []string, defaultEntry string) (string, error) { 183 // Setup the prompt according to the parameters. 184 validStrings := strings.Join(validResponses, "/") 185 var prompt string 186 if defaultEntry != "" { 187 prompt = fmt.Sprintf("%s (%s) [%s]: ", prefix, validStrings, 188 defaultEntry) 189 } else { 190 prompt = fmt.Sprintf("%s (%s): ", prefix, validStrings) 191 } 192 193 // Prompt the user until one of the valid responses is given. 194 for { 195 fmt.Print(prompt) 196 reply, err := reader.ReadString('\n') 197 if err != nil { 198 return "", err 199 } 200 reply = strings.TrimSpace(strings.ToLower(reply)) 201 if reply == "" { 202 reply = defaultEntry 203 } 204 205 for _, validResponse := range validResponses { 206 if reply == validResponse { 207 return reply, nil 208 } 209 } 210 } 211 } 212 213 // promptListBool prompts the user for a boolean (yes/no) with the given 214 // prefix. The function will repeat the prompt to the user until they enter a 215 // valid response. 216 func promptListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) { 217 // Setup the valid responses. 218 valid := []string{"n", "no", "y", "yes"} 219 response, err := promptList(reader, prefix, valid, defaultEntry) 220 if err != nil { 221 return false, err 222 } 223 return response == "yes" || response == "y", nil 224 } 225 226 func validateParseCSV(data []byte) (*cms.InvoiceInput, error) { 227 LineItemType := map[string]cms.LineItemTypeT{ 228 "labor": cms.LineItemTypeLabor, 229 "expense": cms.LineItemTypeExpense, 230 "misc": cms.LineItemTypeMisc, 231 "sub": cms.LineItemTypeSubHours, 232 } 233 invInput := &cms.InvoiceInput{} 234 235 // Validate that the invoice is CSV-formatted. 236 csvReader := csv.NewReader(strings.NewReader(string(data))) 237 csvReader.Comma = cms.PolicyInvoiceFieldDelimiterChar 238 csvReader.Comment = cms.PolicyInvoiceCommentChar 239 csvReader.TrimLeadingSpace = true 240 241 csvFields, err := csvReader.ReadAll() 242 if err != nil { 243 return invInput, err 244 } 245 246 lineItems := make([]cms.LineItemsInput, 0, len(csvFields)) 247 // Validate that line items are the correct length and contents in 248 // field 4 and 5 are parsable to integers 249 for i, lineContents := range csvFields { 250 lineItem := cms.LineItemsInput{} 251 if len(lineContents) != cms.PolicyInvoiceLineItemCount { 252 return invInput, 253 fmt.Errorf("invalid number of line items on line: %v want: %v got: %v", 254 i, cms.PolicyInvoiceLineItemCount, len(lineContents)) 255 } 256 hours, err := strconv.Atoi(lineContents[5]) 257 if err != nil { 258 return invInput, 259 fmt.Errorf("invalid line item hours entered on line: %v", i) 260 } 261 cost, err := strconv.Atoi(lineContents[6]) 262 if err != nil { 263 return invInput, 264 fmt.Errorf("invalid cost entered on line: %v", i) 265 } 266 rate, err := strconv.Atoi(lineContents[8]) 267 if err != nil { 268 return invInput, 269 fmt.Errorf("invalid subrate hours entered on line: %v", i) 270 } 271 lineItemType, ok := LineItemType[strings.ToLower(lineContents[0])] 272 if !ok { 273 return invInput, 274 fmt.Errorf("invalid line item type on line: %v", i) 275 } 276 277 lineItem.Type = lineItemType 278 lineItem.Domain = lineContents[1] 279 lineItem.Subdomain = lineContents[2] 280 lineItem.Description = lineContents[3] 281 lineItem.ProposalToken = lineContents[4] 282 lineItem.SubUserID = lineContents[7] 283 lineItem.SubRate = uint(rate * 100) 284 lineItem.Labor = uint(hours * 60) 285 lineItem.Expenses = uint(cost * 100) 286 lineItems = append(lineItems, lineItem) 287 } 288 invInput.LineItems = lineItems 289 290 return invInput, nil 291 } 292 293 // verifyDCC verifies a dcc's merkle root, author signature, and censorship 294 // record. 295 func verifyDCC(p cms.DCCRecord, serverPubKey string) error { 296 files := make([]pi.File, 0, 1) 297 files = append(files, p.File) 298 // Verify digests 299 err := validateDigests(files, nil) 300 if err != nil { 301 return err 302 } 303 // Verify merkel root 304 mr, err := merkleRoot(files, nil) 305 if err != nil { 306 return err 307 } 308 if mr != p.CensorshipRecord.Merkle { 309 return fmt.Errorf("merkle roots do not match") 310 } 311 312 // Verify dcc signature 313 pid, err := identity.PublicIdentityFromString(p.PublicKey) 314 if err != nil { 315 return err 316 } 317 sig, err := util.ConvertSignature(p.Signature) 318 if err != nil { 319 return err 320 } 321 if !pid.VerifyMessage([]byte(p.CensorshipRecord.Merkle), sig) { 322 return fmt.Errorf("could not verify dcc signature") 323 } 324 325 // Verify censorship record signature 326 id, err := identity.PublicIdentityFromString(serverPubKey) 327 if err != nil { 328 return err 329 } 330 s, err := util.ConvertSignature(p.CensorshipRecord.Signature) 331 if err != nil { 332 return err 333 } 334 msg := []byte(p.CensorshipRecord.Merkle + p.CensorshipRecord.Token) 335 if !id.VerifyMessage(msg, s) { 336 return fmt.Errorf("could not verify censorship record signature") 337 } 338 339 return nil 340 } 341 342 func _main() error { 343 // Load config 344 _cfg, err := shared.LoadConfig(defaultHomeDir, 345 defaultDataDirname, defaultConfigFilename) 346 if err != nil { 347 return fmt.Errorf("loading config: %v", err) 348 } 349 350 // Load client 351 _client, err := shared.NewClient(_cfg) 352 if err != nil { 353 return fmt.Errorf("loading client: %v", err) 354 } 355 356 // Setup global variables for cmswww commands 357 cfg = _cfg 358 client = _client 359 360 // Setup global variables for shared commands 361 shared.SetConfig(_cfg) 362 shared.SetClient(_client) 363 364 // Get politeiawww CSRF token 365 if cfg.CSRF == "" { 366 _, err := client.Version() 367 if err != nil { 368 var e *url.Error 369 if !errors.As(err, &e) { 370 // A url error likely means that politeiawww is not 371 // running. The user may just be trying to print the 372 // help message so only return an error if its not 373 // a url error. 374 return fmt.Errorf("Version: %v", err) 375 } 376 } 377 } 378 379 // Parse subcommand and execute 380 var cli cmswww 381 var parser = flags.NewParser(&cli, flags.Default) 382 if _, err := parser.Parse(); err != nil { 383 var flagsErr *flags.Error 384 if errors.As(err, &flagsErr) && flagsErr.Type == flags.ErrHelp { 385 os.Exit(0) 386 } else { 387 os.Exit(1) 388 } 389 } 390 391 return nil 392 } 393 394 func main() { 395 err := _main() 396 if err != nil { 397 fmt.Fprintf(os.Stderr, "%v\n", err) 398 os.Exit(1) 399 } 400 }