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  }