github.com/decred/politeia@v1.4.0/politeiad/api/v1/v1.go (about) 1 // Copyright (c) 2017-2019 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 v1 6 7 import ( 8 "crypto/sha256" 9 "encoding/base64" 10 "encoding/hex" 11 "errors" 12 "regexp" 13 14 "github.com/decred/dcrtime/merkle" 15 "github.com/decred/politeia/politeiad/api/v1/identity" 16 "github.com/decred/politeia/politeiad/api/v1/mime" 17 ) 18 19 type ErrorStatusT int 20 type RecordStatusT int 21 22 const ( 23 // Routes 24 IdentityRoute = "/v1/identity/" // Retrieve identity 25 NewRecordRoute = "/v1/newrecord/" // New record 26 UpdateUnvettedRoute = "/v1/updateunvetted/" // Update unvetted record 27 UpdateVettedRoute = "/v1/updatevetted/" // Update vetted record 28 UpdateVettedMetadataRoute = "/v1/updatevettedmd/" // Update vetted metadata 29 GetUnvettedRoute = "/v1/getunvetted/" // Retrieve unvetted record 30 GetVettedRoute = "/v1/getvetted/" // Retrieve vetted record 31 32 // Auth required 33 InventoryRoute = "/v1/inventory/" // Inventory records 34 SetUnvettedStatusRoute = "/v1/setunvettedstatus/" // Set unvetted status 35 SetVettedStatusRoute = "/v1/setvettedstatus/" // Set vetted status 36 PluginCommandRoute = "/v1/plugin/" // Send a command to a plugin 37 PluginInventoryRoute = PluginCommandRoute + "inventory/" // Inventory all plugins 38 UpdateReadmeRoute = "/v1/updatereadme/" // Update README 39 40 ChallengeSize = 32 // Size of challenge token in bytes 41 TokenSize = 32 // Size of token 42 MetadataStreamsMax = uint64(16) // Maximum number of metadata streams 43 44 // Error status codes 45 ErrorStatusInvalid ErrorStatusT = 0 46 ErrorStatusInvalidRequestPayload ErrorStatusT = 1 47 ErrorStatusInvalidChallenge ErrorStatusT = 2 48 ErrorStatusInvalidFilename ErrorStatusT = 3 49 ErrorStatusInvalidFileDigest ErrorStatusT = 4 50 ErrorStatusInvalidBase64 ErrorStatusT = 5 51 ErrorStatusInvalidMIMEType ErrorStatusT = 6 52 ErrorStatusUnsupportedMIMEType ErrorStatusT = 7 53 ErrorStatusInvalidRecordStatusTransition ErrorStatusT = 8 54 ErrorStatusEmpty ErrorStatusT = 9 55 ErrorStatusInvalidMDID ErrorStatusT = 10 56 ErrorStatusDuplicateMDID ErrorStatusT = 11 57 ErrorStatusDuplicateFilename ErrorStatusT = 12 58 ErrorStatusFileNotFound ErrorStatusT = 13 59 ErrorStatusNoChanges ErrorStatusT = 14 60 ErrorStatusRecordFound ErrorStatusT = 15 61 ErrorStatusInvalidRPCCredentials ErrorStatusT = 16 62 ErrorStatusLast ErrorStatusT = 17 63 64 // Record status codes (set and get) 65 RecordStatusInvalid RecordStatusT = 0 // Invalid status 66 RecordStatusNotFound RecordStatusT = 1 // Record not found 67 RecordStatusNotReviewed RecordStatusT = 2 // Record has not been reviewed 68 RecordStatusCensored RecordStatusT = 3 // Record has been censored 69 RecordStatusPublic RecordStatusT = 4 // Record is publicly visible 70 RecordStatusUnreviewedChanges RecordStatusT = 5 // Unvetted record that has been changed 71 RecordStatusArchived RecordStatusT = 6 // Vetted record that has been archived 72 RecordStatusLast RecordStatusT = 7 // Unit test only 73 74 // Default network bits 75 DefaultMainnetHost = "politeia.decred.org" 76 DefaultMainnetPort = "49374" 77 DefaultTestnetHost = "politeia-testnet.decred.org" 78 DefaultTestnetPort = "59374" 79 80 Forward = "X-Forwarded-For" 81 ) 82 83 var ( 84 // ErrorStatus converts error status codes to human readable text. 85 ErrorStatus = map[ErrorStatusT]string{ 86 ErrorStatusInvalid: "invalid status", 87 ErrorStatusInvalidRequestPayload: "invalid request payload", 88 ErrorStatusInvalidChallenge: "invalid challenge", 89 ErrorStatusInvalidFilename: "invalid filename", 90 ErrorStatusInvalidFileDigest: "invalid file digest", 91 ErrorStatusInvalidBase64: "corrupt base64 string", 92 ErrorStatusInvalidMIMEType: "invalid MIME type detected", 93 ErrorStatusUnsupportedMIMEType: "unsupported MIME type", 94 ErrorStatusInvalidRecordStatusTransition: "invalid record status transition", 95 ErrorStatusEmpty: "empty record", 96 ErrorStatusInvalidMDID: "invalid metadata id", 97 ErrorStatusDuplicateMDID: "duplicate metadata id", 98 ErrorStatusDuplicateFilename: "duplicate filename", 99 ErrorStatusFileNotFound: "file not found", 100 ErrorStatusNoChanges: "no changes in record", 101 ErrorStatusRecordFound: "record found", 102 ErrorStatusInvalidRPCCredentials: "invalid RPC client credentials", 103 } 104 105 // RecordStatus converts record status codes to human readable text. 106 RecordStatus = map[RecordStatusT]string{ 107 RecordStatusInvalid: "invalid status", 108 RecordStatusNotFound: "not found", 109 RecordStatusNotReviewed: "not reviewed", 110 RecordStatusCensored: "censored", 111 RecordStatusPublic: "public", 112 RecordStatusUnreviewedChanges: "unreviewed changes", 113 RecordStatusArchived: "archived", 114 } 115 116 // Input validation 117 RegexpSHA256 = regexp.MustCompile("[A-Fa-f0-9]{64}") 118 119 // Verification errors 120 ErrInvalidHex = errors.New("corrupt hex string") 121 ErrInvalidBase64 = errors.New("corrupt base64") 122 ErrInvalidMerkle = errors.New("merkle roots do not match") 123 ErrCorrupt = errors.New("signature verification failed") 124 125 // Length of prefix of token used for lookups. The length 7 was selected to 126 // match github's abbreviated hash length This is a var so that it can be 127 // updated during testing. 128 TokenPrefixLength = 7 129 ) 130 131 // Verify ensures that a CensorshipRecord properly describes the array of 132 // files. 133 func Verify(pid identity.PublicIdentity, csr CensorshipRecord, files []File) error { 134 digests := make([]*[sha256.Size]byte, 0, len(files)) 135 for _, file := range files { 136 payload, err := base64.StdEncoding.DecodeString(file.Payload) 137 if err != nil { 138 return ErrInvalidBase64 139 } 140 141 // MIME 142 mimeType := mime.DetectMimeType(payload) 143 if !mime.MimeValid(mimeType) { 144 return mime.ErrUnsupportedMimeType 145 } 146 147 // Digest 148 h := sha256.New() 149 h.Write(payload) 150 d := h.Sum(nil) 151 var digest [sha256.Size]byte 152 copy(digest[:], d) 153 154 digests = append(digests, &digest) 155 } 156 157 // Verify merkle root 158 root := merkle.Root(digests) 159 if hex.EncodeToString(root[:]) != csr.Merkle { 160 return ErrInvalidMerkle 161 } 162 163 s, err := hex.DecodeString(csr.Signature) 164 if err != nil { 165 return ErrInvalidHex 166 } 167 var signature [identity.SignatureSize]byte 168 copy(signature[:], s) 169 r := hex.EncodeToString(root[:]) 170 if !pid.VerifyMessage([]byte(r+csr.Token), signature) { 171 return ErrCorrupt 172 } 173 174 return nil 175 } 176 177 // CensorshipRecord contains the proof that a record was accepted for review. 178 // The proof is verifiable on the client side. 179 // 180 // The Merkle field contains the ordered merkle root of all files in the record. 181 // The Token field contains a random censorship token that is signed by the 182 // server private key. The token can be used on the client to verify the 183 // authenticity of the CensorshipRecord. 184 type CensorshipRecord struct { 185 Token string `json:"token"` // Censorship token 186 Merkle string `json:"merkle"` // Merkle root of record 187 Signature string `json:"signature"` // Signature of merkle+token 188 } 189 190 // Identity requests the record server identity. 191 type Identity struct { 192 Challenge string `json:"challenge"` // Random challenge 193 } 194 195 // IdentityReply contains the server public identity. 196 type IdentityReply struct { 197 Response string `json:"response"` // Signature of Challenge 198 PublicKey string `json:"publickey"` // Public key 199 } 200 201 // File describes an individual file that is part of the record. The 202 // directory structure must be flattened. The server side SHALL verify MIME 203 // and Digest. 204 type File struct { 205 Name string `json:"name"` // Suggested filename 206 MIME string `json:"mime"` // Mime type 207 Digest string `json:"digest"` // Payload digest 208 Payload string `json:"payload"` // File content 209 } 210 211 // MetadataStream identifies a metadata stream by its identity. 212 type MetadataStream struct { 213 ID uint64 `json:"id"` // Stream identity 214 Payload string `json:"payload"` // String encoded metadata 215 } 216 217 // Record is an entire record and it's content. 218 type Record struct { 219 Status RecordStatusT `json:"status"` // Current status 220 Timestamp int64 `json:"timestamp"` // Last update 221 222 CensorshipRecord CensorshipRecord `json:"censorshiprecord"` 223 224 // User data 225 Version string `json:"version"` // Version of this record 226 Metadata []MetadataStream `json:"metadata"` // Metadata streams 227 Files []File `json:"files"` // Files that make up the record 228 } 229 230 // NewRecord creates a new record. It must include all files that are part of 231 // the record and it may contain an optional metatda record. Thet optional 232 // metadatarecord must be string encoded. 233 type NewRecord struct { 234 Challenge string `json:"challenge"` // Random challenge 235 Metadata []MetadataStream `json:"metadata"` // Metadata streams 236 Files []File `json:"files"` // Files that make up record 237 } 238 239 // NewRecordReply returns the CensorshipRecord that is associated with a valid 240 // record. A valid record is not always going to be published. 241 type NewRecordReply struct { 242 Response string `json:"response"` // Challenge response 243 CensorshipRecord CensorshipRecord `json:"censorshiprecord"` 244 } 245 246 // GetUnvetted requests an unvetted record from the server. 247 type GetUnvetted struct { 248 Challenge string `json:"challenge"` // Random challenge 249 Token string `json:"token"` // Censorship token 250 } 251 252 // GetUnvettedReply returns an unvetted record. It retrieves the censorship 253 // record and the actual files. 254 type GetUnvettedReply struct { 255 Response string `json:"response"` // Challenge response 256 Record Record `json:"record"` 257 } 258 259 // GetVetted requests a vetted record from the server. 260 type GetVetted struct { 261 Challenge string `json:"challenge"` // Random challenge 262 Token string `json:"token"` // Censorship token 263 Version string `json:"version"` // Record version 264 } 265 266 // GetVettedReply returns a vetted record. It retrieves the censorship 267 // record and the latest files in the record. 268 type GetVettedReply struct { 269 Response string `json:"response"` // Challenge response 270 Record Record `json:"record"` 271 } 272 273 // SetUnvettedStatus updates the status of an unvetted record. This is used 274 // to either promote a record to the public viewable repository or to censor 275 // it. Additionally, metadata updates may travel along. 276 type SetUnvettedStatus struct { 277 Challenge string `json:"challenge"` // Random challenge 278 Token string `json:"token"` // Censorship token 279 Status RecordStatusT `json:"status"` // New status of record 280 MDAppend []MetadataStream `json:"mdappend"` // Metadata streams to append 281 MDOverwrite []MetadataStream `json:"mdoverwrite"` // Metadata streams to overwrite 282 } 283 284 // SetUnvettedStatus is a response to a SetUnvettedStatus. It returns the 285 // potentially modified record without the Files. 286 type SetUnvettedStatusReply struct { 287 Response string `json:"response"` // Challenge response 288 } 289 290 // SetVettedStatus updates the status of a vetted record. This is used to 291 // archive a vetted proposal. Additionally, metadata updates may travel along. 292 type SetVettedStatus struct { 293 Challenge string `json:"challenge"` // Random challenge 294 Token string `json:"token"` // Censorship token 295 Status RecordStatusT `json:"status"` // New status of record 296 MDAppend []MetadataStream `json:"mdappend"` // Metadata streams to append 297 MDOverwrite []MetadataStream `json:"mdoverwrite"` // Metadata streams to overwrite 298 } 299 300 // SetVettedStatusReply is a response to SetVettedStatus. It returns the 301 // potentially modified record without the Files. 302 type SetVettedStatusReply struct { 303 Response string `json:"response"` // Challenge response 304 } 305 306 // UpdateRecord update an unvetted record. 307 type UpdateRecord struct { 308 Challenge string `json:"challenge"` // Random challenge 309 Token string `json:"token"` // Censorship token 310 MDAppend []MetadataStream `json:"mdappend"` // Metadata streams to append 311 MDOverwrite []MetadataStream `json:"mdoverwrite"` // Metadata streams to overwrite 312 FilesDel []string `json:"filesdel"` // Files that will be deleted 313 FilesAdd []File `json:"filesadd"` // Files that are modified or added 314 } 315 316 // UpdateRecordReply returns a CensorshipRecord which may or may not have 317 // changed. Metadata only updates do not create a new CensorshipRecord. 318 type UpdateRecordReply struct { 319 Response string `json:"response"` // Challenge response 320 } 321 322 // UpdateVettedMetadata update a vetted metadata. This is allowed for 323 // priviledged users. The record itself may not change. 324 type UpdateVettedMetadata struct { 325 Challenge string `json:"challenge"` // Random challenge 326 Token string `json:"token"` // Censorship token 327 MDAppend []MetadataStream `json:"mdappend"` // Metadata streams to append 328 MDOverwrite []MetadataStream `json:"mdoverwrite"` // Metadata streams to overwrite 329 } 330 331 // UpdateVettedMetadataReply returns a response challenge to an 332 // UpdateVettedMetadata command. 333 type UpdateVettedMetadataReply struct { 334 Response string `json:"response"` // Challenge response 335 } 336 337 // UpdateReadme updated the README.md file in the vetted and unvetted repos. 338 type UpdateReadme struct { 339 Challenge string `json:"challenge"` // Random challenge 340 Content string `json:"content"` // New content of README.md 341 } 342 343 // UpdateReadmeReply returns a response challenge to an 344 // UpdateReadme command. 345 type UpdateReadmeReply struct { 346 Response string `json:"response"` // Challenge response 347 } 348 349 // Inventory sends an (expensive and therefore authenticated) inventory request 350 // for vetted records (master branch) and branches (censored, unpublished etc) 351 // records. This is a very expensive call and should be only issued at start 352 // of day. The client should cache the reply. 353 // The IncludeFiles flag indicates if the records contain the record payload 354 // as well. This can quickly become very large and should only be used when 355 // recovering the client side. 356 type Inventory struct { 357 Challenge string `json:"challenge"` // Random challenge 358 // XXX add IncludeMD 359 IncludeFiles bool `json:"includefiles"` // Include files in records 360 // XXX add BranchesStart 361 VettedCount uint `json:"vettedcount"` // Last N vetted records 362 VettedStart uint `json:"vettedstart"` // Index to begin vetted records count 363 BranchesCount uint `json:"branchescount"` // Last N branches (censored, new etc) 364 AllVersions bool `json:"allversions"` // Return all versions of the proposals 365 } 366 367 // InventoryReply returns vetted and unvetted records. If the Inventory 368 // command had IncludeFiles set to true the returned Records will also include 369 // the record files. This obviously enlarges the payload size and should 370 // therefore be used only in disaster recovery scenarios. 371 type InventoryReply struct { 372 Response string `json:"response"` // Challenge response 373 Vetted []Record `json:"vetted"` // Last N vetted records 374 Branches []Record `json:"branches"` // Last N branches (censored, new etc) 375 } 376 377 // UserErrorReply returns details about an error that occurred while trying to 378 // execute a command due to bad input from the client. 379 type UserErrorReply struct { 380 ErrorCode ErrorStatusT `json:"errorcode"` // Numeric error code 381 ErrorContext []string `json:"errorcontext,omitempty"` // Additional error information 382 } 383 384 // ServerErrorReply returns an error code that can be correlated with 385 // server logs. 386 type ServerErrorReply struct { 387 ErrorCode int64 `json:"code"` // Server error code 388 } 389 390 // PluginSetting is a structure that holds key/value pairs of a plugin setting. 391 type PluginSetting struct { 392 Key string `json:"key"` // Name of setting 393 Value string `json:"value"` // Value of setting 394 } 395 396 // Plugin describes a plugin and its settings. 397 type Plugin struct { 398 ID string `json:"id"` // Identifier 399 Version string `json:"version"` // Version 400 Settings []PluginSetting `json:"settings"` // Settings 401 } 402 403 // PluginInventory retrieves all active plugins and their settings. 404 type PluginInventory struct { 405 Challenge string `json:"challenge"` // Random challenge 406 } 407 408 // PluginInventoryReply returns all plugins and their settings. 409 type PluginInventoryReply struct { 410 Response string `json:"response"` // Challenge response 411 Plugins []Plugin `json:"plugins"` // Plugins and their settings 412 } 413 414 // PluginCommand sends a command to a plugin. 415 type PluginCommand struct { 416 Challenge string `json:"challenge"` // Random challenge 417 ID string `json:"id"` // Plugin identifier 418 Command string `json:"command"` // Command identifier 419 CommandID string `json:"commandid"` // User setable command identifier 420 Payload string `json:"payload"` // Actual command 421 } 422 423 // PluginCommandReply is the reply to a PluginCommand. 424 type PluginCommandReply struct { 425 Response string `json:"response"` // Challenge response 426 ID string `json:"id"` // Plugin identifier 427 Command string `json:"command"` // Command identifier 428 CommandID string `json:"commandid"` // User setable command identifier 429 Payload string `json:"payload"` // Actual command reply 430 }