github.com/decred/politeia@v1.4.0/politeiad/client/pdv2.go (about) 1 // Copyright (c) 2020-2021 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 client 6 7 import ( 8 "bytes" 9 "context" 10 "encoding/base64" 11 "encoding/hex" 12 "encoding/json" 13 "fmt" 14 "net/http" 15 16 "github.com/decred/politeia/politeiad/api/v1/identity" 17 pdv2 "github.com/decred/politeia/politeiad/api/v2" 18 v2 "github.com/decred/politeia/politeiad/api/v2" 19 "github.com/decred/politeia/util" 20 ) 21 22 // RecordNew sends a RecordNew command to the politeiad v2 API. 23 func (c *Client) RecordNew(ctx context.Context, metadata []pdv2.MetadataStream, files []pdv2.File) (*pdv2.Record, error) { 24 // Setup request 25 challenge, err := util.Random(pdv2.ChallengeSize) 26 if err != nil { 27 return nil, err 28 } 29 rn := pdv2.RecordNew{ 30 Challenge: hex.EncodeToString(challenge), 31 Metadata: metadata, 32 Files: files, 33 } 34 35 // Send request 36 resBody, err := c.makeReq(ctx, http.MethodPost, 37 pdv2.APIRoute, pdv2.RouteRecordNew, rn) 38 if err != nil { 39 return nil, err 40 } 41 42 // Decode reply 43 var rnr pdv2.RecordNewReply 44 err = json.Unmarshal(resBody, &rnr) 45 if err != nil { 46 return nil, err 47 } 48 err = util.VerifyChallenge(c.pid, challenge, rnr.Response) 49 if err != nil { 50 return nil, err 51 } 52 53 return &rnr.Record, nil 54 } 55 56 // RecordEdit sends a RecordEdit command to the politeiad v2 API. 57 func (c *Client) RecordEdit(ctx context.Context, token string, mdAppend, mdOverwrite []pdv2.MetadataStream, filesAdd []pdv2.File, filesDel []string) (*pdv2.Record, error) { 58 // Setup request 59 challenge, err := util.Random(pdv2.ChallengeSize) 60 if err != nil { 61 return nil, err 62 } 63 re := pdv2.RecordEdit{ 64 Challenge: hex.EncodeToString(challenge), 65 Token: token, 66 MDAppend: mdAppend, 67 MDOverwrite: mdOverwrite, 68 FilesAdd: filesAdd, 69 FilesDel: filesDel, 70 } 71 72 // Send request 73 resBody, err := c.makeReq(ctx, http.MethodPost, 74 pdv2.APIRoute, pdv2.RouteRecordEdit, re) 75 if err != nil { 76 return nil, err 77 } 78 79 // Decode reply 80 var rer pdv2.RecordEditReply 81 err = json.Unmarshal(resBody, &rer) 82 if err != nil { 83 return nil, err 84 } 85 err = util.VerifyChallenge(c.pid, challenge, rer.Response) 86 if err != nil { 87 return nil, err 88 } 89 90 return &rer.Record, nil 91 } 92 93 // RecordEditMetadata sends a RecordEditMetadata command to the politeiad v2 94 // API. 95 func (c *Client) RecordEditMetadata(ctx context.Context, token string, mdAppend, mdOverwrite []pdv2.MetadataStream) (*pdv2.Record, error) { 96 // Setup request 97 challenge, err := util.Random(pdv2.ChallengeSize) 98 if err != nil { 99 return nil, err 100 } 101 rem := pdv2.RecordEditMetadata{ 102 Challenge: hex.EncodeToString(challenge), 103 Token: token, 104 MDAppend: mdAppend, 105 MDOverwrite: mdOverwrite, 106 } 107 108 // Send request 109 resBody, err := c.makeReq(ctx, http.MethodPost, 110 pdv2.APIRoute, pdv2.RouteRecordEditMetadata, rem) 111 if err != nil { 112 return nil, err 113 } 114 115 // Decode reply 116 var reply pdv2.RecordEditMetadataReply 117 err = json.Unmarshal(resBody, &reply) 118 if err != nil { 119 return nil, err 120 } 121 err = util.VerifyChallenge(c.pid, challenge, reply.Response) 122 if err != nil { 123 return nil, err 124 } 125 126 return &reply.Record, nil 127 } 128 129 // RecordSetStatus sends a RecordSetStatus command to the politeiad v2 API. 130 func (c *Client) RecordSetStatus(ctx context.Context, token string, status pdv2.RecordStatusT, mdAppend, mdOverwrite []pdv2.MetadataStream) (*pdv2.Record, error) { 131 // Setup request 132 challenge, err := util.Random(pdv2.ChallengeSize) 133 if err != nil { 134 return nil, err 135 } 136 rss := pdv2.RecordSetStatus{ 137 Challenge: hex.EncodeToString(challenge), 138 Token: token, 139 Status: status, 140 MDAppend: mdAppend, 141 MDOverwrite: mdOverwrite, 142 } 143 144 // Send request 145 resBody, err := c.makeReq(ctx, http.MethodPost, 146 pdv2.APIRoute, pdv2.RouteRecordSetStatus, rss) 147 if err != nil { 148 return nil, err 149 } 150 151 // Decode reply 152 var reply pdv2.RecordSetStatusReply 153 err = json.Unmarshal(resBody, &reply) 154 if err != nil { 155 return nil, err 156 } 157 err = util.VerifyChallenge(c.pid, challenge, reply.Response) 158 if err != nil { 159 return nil, err 160 } 161 162 return &reply.Record, nil 163 } 164 165 // RecordTimestamps sends a RecordTimestamps command to the politeiad v2 API. 166 func (c *Client) RecordTimestamps(ctx context.Context, token string, version uint32) (*pdv2.RecordTimestampsReply, error) { 167 // Setup request 168 challenge, err := util.Random(pdv2.ChallengeSize) 169 if err != nil { 170 return nil, err 171 } 172 rgt := pdv2.RecordTimestamps{ 173 Challenge: hex.EncodeToString(challenge), 174 Token: token, 175 Version: version, 176 } 177 178 // Send request 179 resBody, err := c.makeReq(ctx, http.MethodPost, 180 pdv2.APIRoute, pdv2.RouteRecordTimestamps, rgt) 181 if err != nil { 182 return nil, err 183 } 184 185 // Decode reply 186 var reply pdv2.RecordTimestampsReply 187 err = json.Unmarshal(resBody, &reply) 188 if err != nil { 189 return nil, err 190 } 191 err = util.VerifyChallenge(c.pid, challenge, reply.Response) 192 if err != nil { 193 return nil, err 194 } 195 196 return &reply, nil 197 } 198 199 // Records sends a Records command to the politeiad v2 API. 200 func (c *Client) Records(ctx context.Context, reqs []pdv2.RecordRequest) (map[string]pdv2.Record, error) { 201 // Setup request 202 challenge, err := util.Random(pdv2.ChallengeSize) 203 if err != nil { 204 return nil, err 205 } 206 rgb := pdv2.Records{ 207 Challenge: hex.EncodeToString(challenge), 208 Requests: reqs, 209 } 210 211 // Send request 212 resBody, err := c.makeReq(ctx, http.MethodPost, 213 pdv2.APIRoute, pdv2.RouteRecords, rgb) 214 if err != nil { 215 return nil, err 216 } 217 218 // Decode reply 219 var reply pdv2.RecordsReply 220 err = json.Unmarshal(resBody, &reply) 221 if err != nil { 222 return nil, err 223 } 224 err = util.VerifyChallenge(c.pid, challenge, reply.Response) 225 if err != nil { 226 return nil, err 227 } 228 229 return reply.Records, nil 230 } 231 232 // Inventory sends a Inventory command to the politeiad v2 API. 233 func (c *Client) Inventory(ctx context.Context, state pdv2.RecordStateT, status pdv2.RecordStatusT, page uint32) (*pdv2.InventoryReply, error) { 234 // Setup request 235 challenge, err := util.Random(pdv2.ChallengeSize) 236 if err != nil { 237 return nil, err 238 } 239 i := pdv2.Inventory{ 240 Challenge: hex.EncodeToString(challenge), 241 State: state, 242 Status: status, 243 Page: page, 244 } 245 246 // Send request 247 resBody, err := c.makeReq(ctx, http.MethodPost, 248 pdv2.APIRoute, pdv2.RouteInventory, i) 249 if err != nil { 250 return nil, err 251 } 252 253 // Decode reply 254 var ir pdv2.InventoryReply 255 err = json.Unmarshal(resBody, &ir) 256 if err != nil { 257 return nil, err 258 } 259 err = util.VerifyChallenge(c.pid, challenge, ir.Response) 260 if err != nil { 261 return nil, err 262 } 263 264 return &ir, nil 265 } 266 267 // InventoryOrdered sends a InventoryOrdered command to the politeiad v2 API. 268 func (c *Client) InventoryOrdered(ctx context.Context, state pdv2.RecordStateT, page uint32) ([]string, error) { 269 // Setup request 270 challenge, err := util.Random(pdv2.ChallengeSize) 271 if err != nil { 272 return nil, err 273 } 274 i := pdv2.InventoryOrdered{ 275 Challenge: hex.EncodeToString(challenge), 276 State: state, 277 Page: page, 278 } 279 280 // Send request 281 resBody, err := c.makeReq(ctx, http.MethodPost, 282 pdv2.APIRoute, pdv2.RouteInventoryOrdered, i) 283 if err != nil { 284 return nil, err 285 } 286 287 // Decode reply 288 var ir pdv2.InventoryOrderedReply 289 err = json.Unmarshal(resBody, &ir) 290 if err != nil { 291 return nil, err 292 } 293 err = util.VerifyChallenge(c.pid, challenge, ir.Response) 294 if err != nil { 295 return nil, err 296 } 297 298 return ir.Tokens, nil 299 } 300 301 // PluginWrite sends a PluginWrite command to the politeiad v2 API. 302 func (c *Client) PluginWrite(ctx context.Context, cmd pdv2.PluginCmd) (string, error) { 303 // Setup request 304 challenge, err := util.Random(pdv2.ChallengeSize) 305 if err != nil { 306 return "", err 307 } 308 pw := pdv2.PluginWrite{ 309 Challenge: hex.EncodeToString(challenge), 310 Cmd: cmd, 311 } 312 313 // Send request 314 resBody, err := c.makeReq(ctx, http.MethodPost, 315 pdv2.APIRoute, pdv2.RoutePluginWrite, pw) 316 if err != nil { 317 return "", err 318 } 319 320 // Decode reply 321 var pwr pdv2.PluginWriteReply 322 err = json.Unmarshal(resBody, &pwr) 323 if err != nil { 324 return "", err 325 } 326 err = util.VerifyChallenge(c.pid, challenge, pwr.Response) 327 if err != nil { 328 return "", err 329 } 330 331 return pwr.Payload, nil 332 } 333 334 // PluginReads sends a PluginReads command to the politeiad v2 API. 335 func (c *Client) PluginReads(ctx context.Context, cmds []pdv2.PluginCmd) ([]pdv2.PluginCmdReply, error) { 336 // Setup request 337 challenge, err := util.Random(pdv2.ChallengeSize) 338 if err != nil { 339 return nil, err 340 } 341 pr := pdv2.PluginReads{ 342 Challenge: hex.EncodeToString(challenge), 343 Cmds: cmds, 344 } 345 346 // Send request 347 resBody, err := c.makeReq(ctx, http.MethodPost, 348 pdv2.APIRoute, pdv2.RoutePluginReads, pr) 349 if err != nil { 350 return nil, err 351 } 352 353 // Decode reply 354 var prr pdv2.PluginReadsReply 355 err = json.Unmarshal(resBody, &prr) 356 if err != nil { 357 return nil, err 358 } 359 err = util.VerifyChallenge(c.pid, challenge, prr.Response) 360 if err != nil { 361 return nil, err 362 } 363 364 return prr.Replies, nil 365 } 366 367 // PluginInventory sends a PluginInventory command to the politeiad v2 API. 368 func (c *Client) PluginInventory(ctx context.Context) ([]pdv2.Plugin, error) { 369 // Setup request 370 challenge, err := util.Random(pdv2.ChallengeSize) 371 if err != nil { 372 return nil, err 373 } 374 pi := pdv2.PluginInventory{ 375 Challenge: hex.EncodeToString(challenge), 376 } 377 378 // Send request 379 resBody, err := c.makeReq(ctx, http.MethodPost, 380 pdv2.APIRoute, pdv2.RoutePluginInventory, pi) 381 if err != nil { 382 return nil, err 383 } 384 385 // Decode reply 386 var pir pdv2.PluginInventoryReply 387 err = json.Unmarshal(resBody, &pir) 388 if err != nil { 389 return nil, err 390 } 391 err = util.VerifyChallenge(c.pid, challenge, pir.Response) 392 if err != nil { 393 return nil, err 394 } 395 396 return pir.Plugins, nil 397 } 398 399 // RecordVerify verifies the censorship record of a v2 Record. 400 func RecordVerify(r pdv2.Record, serverPubKey string) error { 401 // Verify censorship record merkle root 402 if len(r.Files) > 0 { 403 // Verify digests 404 err := digestsVerify(r.Files) 405 if err != nil { 406 return err 407 } 408 409 // Verify merkle root 410 digests := make([]string, 0, len(r.Files)) 411 for _, v := range r.Files { 412 digests = append(digests, v.Digest) 413 } 414 mr, err := util.MerkleRoot(digests) 415 if err != nil { 416 return err 417 } 418 if hex.EncodeToString(mr[:]) != r.CensorshipRecord.Merkle { 419 return fmt.Errorf("merkle roots do not match") 420 } 421 } 422 423 // Verify censorship record signature 424 id, err := identity.PublicIdentityFromString(serverPubKey) 425 if err != nil { 426 return err 427 } 428 s, err := util.ConvertSignature(r.CensorshipRecord.Signature) 429 if err != nil { 430 return err 431 } 432 msg := []byte(r.CensorshipRecord.Merkle + r.CensorshipRecord.Token) 433 if !id.VerifyMessage(msg, s) { 434 return fmt.Errorf("invalid censorship record signature") 435 } 436 437 return nil 438 } 439 440 // digestsVerify verifies that all file digests match the calculated SHA256 441 // digests of the file payloads. 442 func digestsVerify(files []v2.File) error { 443 for _, f := range files { 444 b, err := base64.StdEncoding.DecodeString(f.Payload) 445 if err != nil { 446 return fmt.Errorf("file: %v decode payload err %v", 447 f.Name, err) 448 } 449 digest := util.Digest(b) 450 d, ok := util.ConvertDigest(f.Digest) 451 if !ok { 452 return fmt.Errorf("file: %v invalid digest %v", 453 f.Name, f.Digest) 454 } 455 if !bytes.Equal(digest, d[:]) { 456 return fmt.Errorf("file: %v digests do not match", 457 f.Name) 458 } 459 } 460 return nil 461 } 462 463 func extractPluginCmdError(pcr pdv2.PluginCmdReply) error { 464 switch { 465 case pcr.UserError != nil: 466 return RespError{ 467 HTTPCode: http.StatusBadRequest, 468 ErrorReply: ErrorReply{ 469 ErrorCode: uint32(pcr.UserError.ErrorCode), 470 ErrorContext: pcr.UserError.ErrorContext, 471 }, 472 } 473 case pcr.PluginError != nil: 474 return RespError{ 475 HTTPCode: http.StatusBadRequest, 476 ErrorReply: ErrorReply{ 477 PluginID: pcr.PluginError.PluginID, 478 ErrorCode: pcr.PluginError.ErrorCode, 479 ErrorContext: pcr.PluginError.ErrorContext, 480 }, 481 } 482 } 483 return nil 484 }