github.com/decred/politeia@v1.4.0/politeiad/v2.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  package main
     5  
     6  import (
     7  	"encoding/hex"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"net/http"
    12  	"runtime/debug"
    13  	"time"
    14  
    15  	v2 "github.com/decred/politeia/politeiad/api/v2"
    16  	"github.com/decred/politeia/politeiad/backendv2"
    17  	"github.com/decred/politeia/util"
    18  )
    19  
    20  func (p *politeia) handleRecordNew(w http.ResponseWriter, r *http.Request) {
    21  	log.Tracef("handleRecordNew")
    22  
    23  	// Decode request
    24  	var rn v2.RecordNew
    25  	decoder := json.NewDecoder(r.Body)
    26  	if err := decoder.Decode(&rn); err != nil {
    27  		respondWithErrorV2(w, r, "handleRecordNew: unmarshal",
    28  			v2.UserErrorReply{
    29  				ErrorCode: v2.ErrorCodeRequestPayloadInvalid,
    30  			})
    31  		return
    32  	}
    33  	challenge, err := hex.DecodeString(rn.Challenge)
    34  	if err != nil || len(challenge) != v2.ChallengeSize {
    35  		respondWithErrorV2(w, r, "handleRecordNew: decode challenge",
    36  			v2.UserErrorReply{
    37  				ErrorCode: v2.ErrorCodeChallengeInvalid,
    38  			})
    39  		return
    40  	}
    41  
    42  	// Create new record
    43  	var (
    44  		metadata = convertMetadataStreamsToBackend(rn.Metadata)
    45  		files    = convertFilesToBackend(rn.Files)
    46  	)
    47  	rc, err := p.backendv2.RecordNew(metadata, files)
    48  	if err != nil {
    49  		respondWithErrorV2(w, r,
    50  			"handleRecordNew: RecordNew: %v", err)
    51  		return
    52  	}
    53  
    54  	// Prepare reply
    55  	response := p.identity.SignMessage(challenge)
    56  	rnr := v2.RecordNewReply{
    57  		Response: hex.EncodeToString(response[:]),
    58  		Record:   p.convertRecordToV2(*rc),
    59  	}
    60  
    61  	log.Infof("%v Record created %v",
    62  		util.RemoteAddr(r), rc.RecordMetadata.Token)
    63  
    64  	util.RespondWithJSON(w, http.StatusOK, rnr)
    65  }
    66  
    67  func (p *politeia) handleRecordEdit(w http.ResponseWriter, r *http.Request) {
    68  	log.Tracef("handleRecordEdit")
    69  
    70  	// Decode request
    71  	var re v2.RecordEdit
    72  	decoder := json.NewDecoder(r.Body)
    73  	if err := decoder.Decode(&re); err != nil {
    74  		respondWithErrorV2(w, r, "handleRecordEdit: unmarshal",
    75  			v2.UserErrorReply{
    76  				ErrorCode: v2.ErrorCodeRequestPayloadInvalid,
    77  			})
    78  		return
    79  	}
    80  	challenge, err := hex.DecodeString(re.Challenge)
    81  	if err != nil || len(challenge) != v2.ChallengeSize {
    82  		respondWithErrorV2(w, r, "handleRecordEdit: decode challenge",
    83  			v2.UserErrorReply{
    84  				ErrorCode: v2.ErrorCodeChallengeInvalid,
    85  			})
    86  		return
    87  	}
    88  	token, err := decodeToken(re.Token)
    89  	if err != nil {
    90  		respondWithErrorV2(w, r, "handleRecordEdit: decode token",
    91  			v2.UserErrorReply{
    92  				ErrorCode:    v2.ErrorCodeTokenInvalid,
    93  				ErrorContext: util.TokenRegexp(),
    94  			})
    95  		return
    96  	}
    97  
    98  	// Edit record
    99  	var (
   100  		mdAppend    = convertMetadataStreamsToBackend(re.MDAppend)
   101  		mdOverwrite = convertMetadataStreamsToBackend(re.MDOverwrite)
   102  		filesAdd    = convertFilesToBackend(re.FilesAdd)
   103  	)
   104  	rc, err := p.backendv2.RecordEdit(token, mdAppend,
   105  		mdOverwrite, filesAdd, re.FilesDel)
   106  	if err != nil {
   107  		respondWithErrorV2(w, r,
   108  			"handleRecordEdit: RecordEdit: %v", err)
   109  		return
   110  	}
   111  
   112  	// Prepare reply
   113  	response := p.identity.SignMessage(challenge)
   114  	rer := v2.RecordEditReply{
   115  		Response: hex.EncodeToString(response[:]),
   116  		Record:   p.convertRecordToV2(*rc),
   117  	}
   118  
   119  	log.Infof("%v Record edited %v",
   120  		util.RemoteAddr(r), rc.RecordMetadata.Token)
   121  
   122  	util.RespondWithJSON(w, http.StatusOK, rer)
   123  }
   124  
   125  func (p *politeia) handleRecordEditMetadata(w http.ResponseWriter, r *http.Request) {
   126  	log.Tracef("handleRecordEditMetadata")
   127  
   128  	// Decode request
   129  	var re v2.RecordEditMetadata
   130  	decoder := json.NewDecoder(r.Body)
   131  	if err := decoder.Decode(&re); err != nil {
   132  		respondWithErrorV2(w, r, "handleRecordEditMetadata: unmarshal",
   133  			v2.UserErrorReply{
   134  				ErrorCode: v2.ErrorCodeRequestPayloadInvalid,
   135  			})
   136  		return
   137  	}
   138  	challenge, err := hex.DecodeString(re.Challenge)
   139  	if err != nil || len(challenge) != v2.ChallengeSize {
   140  		respondWithErrorV2(w, r, "handleRecordEditMetadata: decode challenge",
   141  			v2.UserErrorReply{
   142  				ErrorCode: v2.ErrorCodeChallengeInvalid,
   143  			})
   144  		return
   145  	}
   146  	token, err := decodeToken(re.Token)
   147  	if err != nil {
   148  		respondWithErrorV2(w, r, "handleRecordEditMetadata: decode token",
   149  			v2.UserErrorReply{
   150  				ErrorCode:    v2.ErrorCodeTokenInvalid,
   151  				ErrorContext: util.TokenRegexp(),
   152  			})
   153  		return
   154  	}
   155  
   156  	// Edit record metadata
   157  	var (
   158  		mdAppend    = convertMetadataStreamsToBackend(re.MDAppend)
   159  		mdOverwrite = convertMetadataStreamsToBackend(re.MDOverwrite)
   160  	)
   161  	rc, err := p.backendv2.RecordEditMetadata(token, mdAppend, mdOverwrite)
   162  	if err != nil {
   163  		respondWithErrorV2(w, r,
   164  			"handleRecordEditMetadata: RecordEditMetadata: %v", err)
   165  		return
   166  	}
   167  
   168  	// Prepare reply
   169  	response := p.identity.SignMessage(challenge)
   170  	rer := v2.RecordEditMetadataReply{
   171  		Response: hex.EncodeToString(response[:]),
   172  		Record:   p.convertRecordToV2(*rc),
   173  	}
   174  
   175  	util.RespondWithJSON(w, http.StatusOK, rer)
   176  }
   177  
   178  func (p *politeia) handleRecordSetStatus(w http.ResponseWriter, r *http.Request) {
   179  	log.Tracef("handleRecordSetStatus")
   180  
   181  	// Decode request
   182  	var rss v2.RecordSetStatus
   183  	decoder := json.NewDecoder(r.Body)
   184  	if err := decoder.Decode(&rss); err != nil {
   185  		respondWithErrorV2(w, r, "handleRecordSetStatus: unmarshal",
   186  			v2.UserErrorReply{
   187  				ErrorCode: v2.ErrorCodeRequestPayloadInvalid,
   188  			})
   189  		return
   190  	}
   191  	challenge, err := hex.DecodeString(rss.Challenge)
   192  	if err != nil || len(challenge) != v2.ChallengeSize {
   193  		respondWithErrorV2(w, r, "handleRecordSetStatus: decode challenge",
   194  			v2.UserErrorReply{
   195  				ErrorCode: v2.ErrorCodeChallengeInvalid,
   196  			})
   197  		return
   198  	}
   199  	token, err := decodeToken(rss.Token)
   200  	if err != nil {
   201  		respondWithErrorV2(w, r, "handleRecordSetStatus: decode token",
   202  			v2.UserErrorReply{
   203  				ErrorCode:    v2.ErrorCodeTokenInvalid,
   204  				ErrorContext: util.TokenRegexp(),
   205  			})
   206  		return
   207  	}
   208  
   209  	// Set record status
   210  	var (
   211  		mdAppend    = convertMetadataStreamsToBackend(rss.MDAppend)
   212  		mdOverwrite = convertMetadataStreamsToBackend(rss.MDOverwrite)
   213  		status      = backendv2.StatusT(rss.Status)
   214  	)
   215  	rc, err := p.backendv2.RecordSetStatus(token, status,
   216  		mdAppend, mdOverwrite)
   217  	if err != nil {
   218  		respondWithErrorV2(w, r,
   219  			"handleRecordSetStatus: RecordSetStatus: %v", err)
   220  		return
   221  	}
   222  
   223  	// Prepare reply
   224  	response := p.identity.SignMessage(challenge)
   225  	rer := v2.RecordSetStatusReply{
   226  		Response: hex.EncodeToString(response[:]),
   227  		Record:   p.convertRecordToV2(*rc),
   228  	}
   229  
   230  	log.Infof("%v Record status set %v %v", util.RemoteAddr(r),
   231  		rc.RecordMetadata.Token, backendv2.Statuses[rc.RecordMetadata.Status])
   232  
   233  	util.RespondWithJSON(w, http.StatusOK, rer)
   234  }
   235  
   236  func (p *politeia) handleRecords(w http.ResponseWriter, r *http.Request) {
   237  	log.Tracef("handleRecords")
   238  
   239  	// Decode request
   240  	var rgb v2.Records
   241  	decoder := json.NewDecoder(r.Body)
   242  	if err := decoder.Decode(&rgb); err != nil {
   243  		respondWithErrorV2(w, r, "handleRecords: unmarshal",
   244  			v2.UserErrorReply{
   245  				ErrorCode: v2.ErrorCodeRequestPayloadInvalid,
   246  			})
   247  		return
   248  	}
   249  	challenge, err := hex.DecodeString(rgb.Challenge)
   250  	if err != nil || len(challenge) != v2.ChallengeSize {
   251  		respondWithErrorV2(w, r, "handleRecords: decode challenge",
   252  			v2.UserErrorReply{
   253  				ErrorCode: v2.ErrorCodeChallengeInvalid,
   254  			})
   255  		return
   256  	}
   257  
   258  	// Verify page size
   259  	if len(rgb.Requests) > int(v2.RecordsPageSize) {
   260  		respondWithErrorV2(w, r, "handleRecords: unmarshal",
   261  			v2.UserErrorReply{
   262  				ErrorCode: v2.ErrorCodePageSizeExceeded,
   263  			})
   264  		return
   265  	}
   266  
   267  	// Get record batch
   268  	reqs := convertRecordRequestsToBackend(rgb.Requests)
   269  	brecords, err := p.backendv2.Records(reqs)
   270  	if err != nil {
   271  		respondWithErrorV2(w, r,
   272  			"handleRecordGet: Records: %v", err)
   273  		return
   274  	}
   275  
   276  	// Prepare reply
   277  	records := make(map[string]v2.Record, len(brecords))
   278  	for k, v := range brecords {
   279  		records[k] = p.convertRecordToV2(v)
   280  	}
   281  	response := p.identity.SignMessage(challenge)
   282  	reply := v2.RecordsReply{
   283  		Response: hex.EncodeToString(response[:]),
   284  		Records:  records,
   285  	}
   286  
   287  	util.RespondWithJSON(w, http.StatusOK, reply)
   288  }
   289  
   290  func (p *politeia) handleRecordTimestamps(w http.ResponseWriter, r *http.Request) {
   291  	log.Tracef("handleRecordTimestamps")
   292  
   293  	// Decode request
   294  	var rgt v2.RecordTimestamps
   295  	decoder := json.NewDecoder(r.Body)
   296  	if err := decoder.Decode(&rgt); err != nil {
   297  		respondWithErrorV2(w, r, "handleRecordTimestamps: unmarshal",
   298  			v2.UserErrorReply{
   299  				ErrorCode: v2.ErrorCodeRequestPayloadInvalid,
   300  			})
   301  		return
   302  	}
   303  	challenge, err := hex.DecodeString(rgt.Challenge)
   304  	if err != nil || len(challenge) != v2.ChallengeSize {
   305  		respondWithErrorV2(w, r, "handleRecordTimestamps: decode challenge",
   306  			v2.UserErrorReply{
   307  				ErrorCode: v2.ErrorCodeChallengeInvalid,
   308  			})
   309  		return
   310  	}
   311  	token, err := decodeTokenAnyLength(rgt.Token)
   312  	if err != nil {
   313  		respondWithErrorV2(w, r, "handleRecordTimestamps: decode token",
   314  			v2.UserErrorReply{
   315  				ErrorCode:    v2.ErrorCodeTokenInvalid,
   316  				ErrorContext: util.TokenRegexp(),
   317  			})
   318  		return
   319  	}
   320  
   321  	// Get record timestamps
   322  	rt, err := p.backendv2.RecordTimestamps(token, rgt.Version)
   323  	if err != nil {
   324  		respondWithErrorV2(w, r,
   325  			"handleRecordTimestamps: RecordTimestamps: %v", err)
   326  		return
   327  	}
   328  
   329  	// Prepare reply
   330  	response := p.identity.SignMessage(challenge)
   331  	rtr := v2.RecordTimestampsReply{
   332  		Response:       hex.EncodeToString(response[:]),
   333  		RecordMetadata: convertTimestampToV2(rt.RecordMetadata),
   334  		Metadata:       convertMetadataTimestampsToV2(rt.Metadata),
   335  		Files:          convertFileTimestampsToV2(rt.Files),
   336  	}
   337  
   338  	util.RespondWithJSON(w, http.StatusOK, rtr)
   339  }
   340  
   341  func (p *politeia) handleInventory(w http.ResponseWriter, r *http.Request) {
   342  	log.Tracef("handleInventory")
   343  
   344  	// Decode request
   345  	var i v2.Inventory
   346  	decoder := json.NewDecoder(r.Body)
   347  	if err := decoder.Decode(&i); err != nil {
   348  		respondWithErrorV2(w, r, "handleInventory: unmarshal",
   349  			v2.UserErrorReply{
   350  				ErrorCode: v2.ErrorCodeRequestPayloadInvalid,
   351  			})
   352  		return
   353  	}
   354  	challenge, err := hex.DecodeString(i.Challenge)
   355  	if err != nil || len(challenge) != v2.ChallengeSize {
   356  		respondWithErrorV2(w, r, "handleInventory: decode challenge",
   357  			v2.UserErrorReply{
   358  				ErrorCode: v2.ErrorCodeChallengeInvalid,
   359  			})
   360  		return
   361  	}
   362  
   363  	// Verify inventory arguments. These arguments are optional. Only
   364  	// return an error if the arguments have been provided.
   365  	var (
   366  		state      backendv2.StateT
   367  		status     backendv2.StatusT
   368  		pageSize   = v2.InventoryPageSize
   369  		pageNumber = i.Page
   370  	)
   371  	if i.State != v2.RecordStateInvalid {
   372  		state = convertRecordStateToBackend(i.State)
   373  		if state == backendv2.StateInvalid {
   374  			respondWithErrorV2(w, r, "",
   375  				v2.UserErrorReply{
   376  					ErrorCode: v2.ErrorCodeRecordStateInvalid,
   377  				})
   378  			return
   379  		}
   380  	}
   381  	if i.Status != v2.RecordStatusInvalid {
   382  		status = convertRecordStatusToBackend(i.Status)
   383  		if status == backendv2.StatusInvalid {
   384  			respondWithErrorV2(w, r, "",
   385  				v2.UserErrorReply{
   386  					ErrorCode: v2.ErrorCodeRecordStatusInvalid,
   387  				})
   388  			return
   389  		}
   390  	}
   391  
   392  	// Get inventory
   393  	inv, err := p.backendv2.Inventory(state, status, pageSize, pageNumber)
   394  	if err != nil {
   395  		respondWithErrorV2(w, r,
   396  			"handleInventory: Inventory: %v", err)
   397  		return
   398  	}
   399  
   400  	// Prepare reply
   401  	unvetted := make(map[string][]string, len(inv.Unvetted))
   402  	for k, v := range inv.Unvetted {
   403  		key := backendv2.Statuses[k]
   404  		unvetted[key] = v
   405  	}
   406  	vetted := make(map[string][]string, len(inv.Vetted))
   407  	for k, v := range inv.Vetted {
   408  		key := backendv2.Statuses[k]
   409  		vetted[key] = v
   410  	}
   411  	response := p.identity.SignMessage(challenge)
   412  	ir := v2.InventoryReply{
   413  		Response: hex.EncodeToString(response[:]),
   414  		Unvetted: unvetted,
   415  		Vetted:   vetted,
   416  	}
   417  
   418  	util.RespondWithJSON(w, http.StatusOK, ir)
   419  }
   420  
   421  func (p *politeia) handleInventoryOrdered(w http.ResponseWriter, r *http.Request) {
   422  	log.Tracef("handleInventoryOrdered")
   423  
   424  	// Decode request
   425  	var i v2.InventoryOrdered
   426  	decoder := json.NewDecoder(r.Body)
   427  	if err := decoder.Decode(&i); err != nil {
   428  		respondWithErrorV2(w, r, "handleInventoryOrdered: unmarshal",
   429  			v2.UserErrorReply{
   430  				ErrorCode: v2.ErrorCodeRequestPayloadInvalid,
   431  			})
   432  		return
   433  	}
   434  	challenge, err := hex.DecodeString(i.Challenge)
   435  	if err != nil || len(challenge) != v2.ChallengeSize {
   436  		respondWithErrorV2(w, r, "handleInventoryOrdered: decode challenge",
   437  			v2.UserErrorReply{
   438  				ErrorCode: v2.ErrorCodeChallengeInvalid,
   439  			})
   440  		return
   441  	}
   442  
   443  	// Verify record state
   444  	var state backendv2.StateT
   445  	if i.State != v2.RecordStateInvalid {
   446  		state = convertRecordStateToBackend(i.State)
   447  		if state == backendv2.StateInvalid {
   448  			respondWithErrorV2(w, r, "",
   449  				v2.UserErrorReply{
   450  					ErrorCode: v2.ErrorCodeRecordStateInvalid,
   451  				})
   452  			return
   453  		}
   454  	}
   455  
   456  	// Get inventory
   457  	tokens, err := p.backendv2.InventoryOrdered(state,
   458  		v2.InventoryPageSize, i.Page)
   459  	if err != nil {
   460  		respondWithErrorV2(w, r,
   461  			"handleInventoryOrdered: InventoryOrdered: %v", err)
   462  		return
   463  	}
   464  
   465  	response := p.identity.SignMessage(challenge)
   466  	ir := v2.InventoryOrderedReply{
   467  		Response: hex.EncodeToString(response[:]),
   468  		Tokens:   tokens,
   469  	}
   470  
   471  	util.RespondWithJSON(w, http.StatusOK, ir)
   472  }
   473  
   474  func (p *politeia) handlePluginWrite(w http.ResponseWriter, r *http.Request) {
   475  	log.Tracef("handlePluginWrite")
   476  
   477  	// Decode request
   478  	var pw v2.PluginWrite
   479  	decoder := json.NewDecoder(r.Body)
   480  	if err := decoder.Decode(&pw); err != nil {
   481  		respondWithErrorV2(w, r, "handlePluginWrite: unmarshal",
   482  			v2.UserErrorReply{
   483  				ErrorCode: v2.ErrorCodeRequestPayloadInvalid,
   484  			})
   485  		return
   486  	}
   487  	challenge, err := hex.DecodeString(pw.Challenge)
   488  	if err != nil || len(challenge) != v2.ChallengeSize {
   489  		respondWithErrorV2(w, r, "handlePluginWrite: decode challenge",
   490  			v2.UserErrorReply{
   491  				ErrorCode: v2.ErrorCodeChallengeInvalid,
   492  			})
   493  		return
   494  	}
   495  	token, err := decodeToken(pw.Cmd.Token)
   496  	if err != nil {
   497  		respondWithErrorV2(w, r, "handlePluginWrite: decode token",
   498  			v2.UserErrorReply{
   499  				ErrorCode:    v2.ErrorCodeTokenInvalid,
   500  				ErrorContext: util.TokenRegexp(),
   501  			})
   502  		return
   503  	}
   504  
   505  	// Execute plugin cmd
   506  	payload, err := p.backendv2.PluginWrite(token, pw.Cmd.ID,
   507  		pw.Cmd.Command, pw.Cmd.Payload)
   508  	if err != nil {
   509  		respondWithErrorV2(w, r,
   510  			"handlePluginWrite: PluginWrite: %v", err)
   511  		return
   512  	}
   513  
   514  	// Prepare reply
   515  	response := p.identity.SignMessage(challenge)
   516  	pwr := v2.PluginWriteReply{
   517  		Response: hex.EncodeToString(response[:]),
   518  		Payload:  payload,
   519  	}
   520  
   521  	log.Infof("%v Plugin '%v' write cmd '%v' executed",
   522  		util.RemoteAddr(r), pw.Cmd.ID, pw.Cmd.Command)
   523  
   524  	util.RespondWithJSON(w, http.StatusOK, pwr)
   525  }
   526  
   527  func (p *politeia) handlePluginReads(w http.ResponseWriter, r *http.Request) {
   528  	log.Tracef("handlePluginReads")
   529  
   530  	// Decode request
   531  	var pr v2.PluginReads
   532  	decoder := json.NewDecoder(r.Body)
   533  	if err := decoder.Decode(&pr); err != nil {
   534  		respondWithErrorV2(w, r, "handlePluginReads: unmarshal",
   535  			v2.UserErrorReply{
   536  				ErrorCode: v2.ErrorCodeRequestPayloadInvalid,
   537  			})
   538  		return
   539  	}
   540  	challenge, err := hex.DecodeString(pr.Challenge)
   541  	if err != nil || len(challenge) != v2.ChallengeSize {
   542  		respondWithErrorV2(w, r, "handlePluginReads: decode challenge",
   543  			v2.UserErrorReply{
   544  				ErrorCode: v2.ErrorCodeChallengeInvalid,
   545  			})
   546  		return
   547  	}
   548  
   549  	// Execute the batch of read cmds
   550  	batch := newBatch(pr.Cmds)
   551  	batch.execConcurrently(p.backendv2.PluginRead)
   552  
   553  	// Prepare the replies
   554  	replies := make([]v2.PluginCmdReply, len(pr.Cmds))
   555  	for k, v := range batch.entries {
   556  		if v.err == nil {
   557  			// Command executed successfully
   558  			replies[k] = v2.PluginCmdReply{
   559  				Token:   v.cmd.Token,
   560  				ID:      v.cmd.ID,
   561  				Command: v.cmd.Command,
   562  				Payload: v.reply,
   563  			}
   564  			continue
   565  		}
   566  
   567  		// An error was encountered. Plugin and user errors
   568  		// are returned in valid replies. Unexpected errors
   569  		// cause a 500 and the whole batch to be aborted.
   570  		var (
   571  			pluginErr   backendv2.PluginError
   572  			userErr     v2.UserErrorReply
   573  			userErrCode = convertErrorToV2(v.err)
   574  		)
   575  		switch {
   576  		case errors.As(v.err, &pluginErr):
   577  			// A plugin error was returned
   578  			replies[k] = v2.PluginCmdReply{
   579  				PluginError: &v2.PluginErrorReply{
   580  					PluginID:     pluginErr.PluginID,
   581  					ErrorCode:    pluginErr.ErrorCode,
   582  					ErrorContext: pluginErr.ErrorContext,
   583  				},
   584  			}
   585  
   586  		case errors.As(v.err, &userErr):
   587  			// A user error was returned
   588  			replies[k] = v2.PluginCmdReply{
   589  				UserError: &userErr,
   590  			}
   591  
   592  		case userErrCode != v2.ErrorCodeInvalid:
   593  			// Backend error was returned that was
   594  			// converted into a valid user error.
   595  			replies[k] = v2.PluginCmdReply{
   596  				UserError: &v2.UserErrorReply{
   597  					ErrorCode: userErrCode,
   598  				},
   599  			}
   600  
   601  		default:
   602  			// Internal server error. Log it and return a 500.
   603  			t := time.Now().Unix()
   604  			e := fmt.Sprintf("PluginRead %v %v %v: %v",
   605  				v.cmd.ID, v.cmd.Command, v.cmd.Payload, v.err)
   606  			log.Errorf("%v %v %v %v Internal error %v: %v",
   607  				util.RemoteAddr(r), r.Method, r.URL, r.Proto, t, e)
   608  			log.Errorf("Stacktrace (NOT A REAL CRASH): %s", debug.Stack())
   609  
   610  			util.RespondWithJSON(w, http.StatusInternalServerError,
   611  				v2.ServerErrorReply{
   612  					ErrorCode: t,
   613  				})
   614  			return
   615  		}
   616  	}
   617  
   618  	// Prepare reply
   619  	response := p.identity.SignMessage(challenge)
   620  	prr := v2.PluginReadsReply{
   621  		Response: hex.EncodeToString(response[:]),
   622  		Replies:  replies,
   623  	}
   624  
   625  	util.RespondWithJSON(w, http.StatusOK, prr)
   626  
   627  }
   628  
   629  func (p *politeia) handlePluginInventory(w http.ResponseWriter, r *http.Request) {
   630  	log.Tracef("handlePluginInventory")
   631  
   632  	// Decode request
   633  	var pi v2.PluginInventory
   634  	decoder := json.NewDecoder(r.Body)
   635  	if err := decoder.Decode(&pi); err != nil {
   636  		respondWithErrorV2(w, r, "handlePluginInventory: unmarshal",
   637  			v2.UserErrorReply{
   638  				ErrorCode: v2.ErrorCodeRequestPayloadInvalid,
   639  			})
   640  		return
   641  	}
   642  	challenge, err := hex.DecodeString(pi.Challenge)
   643  	if err != nil || len(challenge) != v2.ChallengeSize {
   644  		respondWithErrorV2(w, r, "handlePluginInventory: decode challenge",
   645  			v2.UserErrorReply{
   646  				ErrorCode: v2.ErrorCodeChallengeInvalid,
   647  			})
   648  		return
   649  	}
   650  
   651  	// Get plugin inventory
   652  	plugins := p.backendv2.PluginInventory()
   653  
   654  	// Prepare reply
   655  	response := p.identity.SignMessage(challenge)
   656  	ir := v2.PluginInventoryReply{
   657  		Response: hex.EncodeToString(response[:]),
   658  		Plugins:  convertPluginsToV2(plugins),
   659  	}
   660  
   661  	util.RespondWithJSON(w, http.StatusOK, ir)
   662  
   663  }
   664  
   665  // decodeToken decodes a v2 token and errors if the token is not the full
   666  // length token.
   667  func decodeToken(token string) ([]byte, error) {
   668  	return util.TokenDecode(util.TokenTypeTstore, token)
   669  }
   670  
   671  // decodeTokenAnyLength decodes a v2 token. It accepts both the full length
   672  // token and the short token.
   673  func decodeTokenAnyLength(token string) ([]byte, error) {
   674  	return util.TokenDecodeAnyLength(util.TokenTypeTstore, token)
   675  }
   676  
   677  func (p *politeia) convertRecordToV2(r backendv2.Record) v2.Record {
   678  	var (
   679  		metadata = convertMetadataStreamsToV2(r.Metadata)
   680  		files    = convertFilesToV2(r.Files)
   681  		rm       = r.RecordMetadata
   682  		sig      = p.identity.SignMessage([]byte(rm.Merkle + rm.Token))
   683  	)
   684  	return v2.Record{
   685  		State:     v2.RecordStateT(rm.State),
   686  		Status:    v2.RecordStatusT(rm.Status),
   687  		Version:   rm.Version,
   688  		Timestamp: rm.Timestamp,
   689  		Metadata:  metadata,
   690  		Files:     files,
   691  		CensorshipRecord: v2.CensorshipRecord{
   692  			Token:     rm.Token,
   693  			Merkle:    rm.Merkle,
   694  			Signature: hex.EncodeToString(sig[:]),
   695  		},
   696  	}
   697  }
   698  
   699  func convertMetadataStreamsToBackend(metadata []v2.MetadataStream) []backendv2.MetadataStream {
   700  	ms := make([]backendv2.MetadataStream, 0, len(metadata))
   701  	for _, v := range metadata {
   702  		ms = append(ms, backendv2.MetadataStream{
   703  			PluginID: v.PluginID,
   704  			StreamID: v.StreamID,
   705  			Payload:  v.Payload,
   706  		})
   707  	}
   708  	return ms
   709  }
   710  
   711  func convertMetadataStreamsToV2(metadata []backendv2.MetadataStream) []v2.MetadataStream {
   712  	ms := make([]v2.MetadataStream, 0, len(metadata))
   713  	for _, v := range metadata {
   714  		ms = append(ms, v2.MetadataStream{
   715  			PluginID: v.PluginID,
   716  			StreamID: v.StreamID,
   717  			Payload:  v.Payload,
   718  		})
   719  	}
   720  	return ms
   721  }
   722  
   723  func convertFilesToBackend(files []v2.File) []backendv2.File {
   724  	fs := make([]backendv2.File, 0, len(files))
   725  	for _, v := range files {
   726  		fs = append(fs, backendv2.File{
   727  			Name:    v.Name,
   728  			MIME:    v.MIME,
   729  			Digest:  v.Digest,
   730  			Payload: v.Payload,
   731  		})
   732  	}
   733  	return fs
   734  }
   735  
   736  func convertFilesToV2(files []backendv2.File) []v2.File {
   737  	fs := make([]v2.File, 0, len(files))
   738  	for _, v := range files {
   739  		fs = append(fs, v2.File{
   740  			Name:    v.Name,
   741  			MIME:    v.MIME,
   742  			Digest:  v.Digest,
   743  			Payload: v.Payload,
   744  		})
   745  	}
   746  	return fs
   747  }
   748  
   749  func convertRecordRequestsToBackend(reqs []v2.RecordRequest) []backendv2.RecordRequest {
   750  	r := make([]backendv2.RecordRequest, 0, len(reqs))
   751  	for _, v := range reqs {
   752  		token, err := decodeTokenAnyLength(v.Token)
   753  		if err != nil {
   754  			// Records with errors will not be included in the reply
   755  			log.Debugf("convertRecordRequestsToBackend: decode token: %v", err)
   756  			continue
   757  		}
   758  		r = append(r, backendv2.RecordRequest{
   759  			Token:        token,
   760  			Version:      v.Version,
   761  			Filenames:    v.Filenames,
   762  			OmitAllFiles: v.OmitAllFiles,
   763  		})
   764  	}
   765  	return r
   766  }
   767  
   768  func convertProofToV2(p backendv2.Proof) v2.Proof {
   769  	return v2.Proof{
   770  		Type:       p.Type,
   771  		Digest:     p.Digest,
   772  		MerkleRoot: p.MerkleRoot,
   773  		MerklePath: p.MerklePath,
   774  		ExtraData:  p.ExtraData,
   775  	}
   776  }
   777  
   778  func convertTimestampToV2(t backendv2.Timestamp) v2.Timestamp {
   779  	proofs := make([]v2.Proof, 0, len(t.Proofs))
   780  	for _, v := range t.Proofs {
   781  		proofs = append(proofs, convertProofToV2(v))
   782  	}
   783  	return v2.Timestamp{
   784  		Data:       t.Data,
   785  		Digest:     t.Digest,
   786  		TxID:       t.TxID,
   787  		MerkleRoot: t.MerkleRoot,
   788  		Proofs:     proofs,
   789  	}
   790  }
   791  
   792  func convertMetadataTimestampsToV2(metadata map[string]map[uint32]backendv2.Timestamp) map[string]map[uint32]v2.Timestamp {
   793  	md := make(map[string]map[uint32]v2.Timestamp, 16)
   794  	for pluginID, v := range metadata {
   795  		timestamps, ok := md[pluginID]
   796  		if !ok {
   797  			timestamps = make(map[uint32]v2.Timestamp, 16)
   798  		}
   799  		for streamID, ts := range v {
   800  			timestamps[streamID] = convertTimestampToV2(ts)
   801  		}
   802  		md[pluginID] = timestamps
   803  	}
   804  	return md
   805  }
   806  
   807  func convertFileTimestampsToV2(files map[string]backendv2.Timestamp) map[string]v2.Timestamp {
   808  	fs := make(map[string]v2.Timestamp, len(files))
   809  	for k, v := range files {
   810  		fs[k] = convertTimestampToV2(v)
   811  	}
   812  	return fs
   813  }
   814  
   815  func convertRecordStateToBackend(s v2.RecordStateT) backendv2.StateT {
   816  	switch s {
   817  	case v2.RecordStateUnvetted:
   818  		return backendv2.StateUnvetted
   819  	case v2.RecordStateVetted:
   820  		return backendv2.StateVetted
   821  	}
   822  	return backendv2.StateInvalid
   823  }
   824  
   825  func convertRecordStatusToBackend(s v2.RecordStatusT) backendv2.StatusT {
   826  	switch s {
   827  	case v2.RecordStatusUnreviewed:
   828  		return backendv2.StatusUnreviewed
   829  	case v2.RecordStatusPublic:
   830  		return backendv2.StatusPublic
   831  	case v2.RecordStatusCensored:
   832  		return backendv2.StatusCensored
   833  	case v2.RecordStatusArchived:
   834  		return backendv2.StatusArchived
   835  	}
   836  	return backendv2.StatusInvalid
   837  }
   838  
   839  func convertPluginSettingToV2(p backendv2.PluginSetting) v2.PluginSetting {
   840  	return v2.PluginSetting{
   841  		Key:   p.Key,
   842  		Value: p.Value,
   843  	}
   844  }
   845  
   846  func convertPluginsToV2(bplugins []backendv2.Plugin) []v2.Plugin {
   847  	plugins := make([]v2.Plugin, 0, len(bplugins))
   848  	for _, v := range bplugins {
   849  		settings := make([]v2.PluginSetting, 0, len(v.Settings))
   850  		for _, v := range v.Settings {
   851  			settings = append(settings, convertPluginSettingToV2(v))
   852  		}
   853  		plugins = append(plugins, v2.Plugin{
   854  			ID:       v.ID,
   855  			Settings: settings,
   856  		})
   857  	}
   858  	return plugins
   859  }
   860  
   861  func respondWithErrorV2(w http.ResponseWriter, r *http.Request, format string, err error) {
   862  	var (
   863  		errCode = convertErrorToV2(err)
   864  		ue      v2.UserErrorReply
   865  		ce      backendv2.ContentError
   866  		ste     backendv2.StatusTransitionError
   867  		pe      backendv2.PluginError
   868  	)
   869  	switch {
   870  	case errCode != v2.ErrorCodeInvalid:
   871  		// Backend error
   872  		log.Infof("%v User error: %v %v", util.RemoteAddr(r),
   873  			errCode, v2.ErrorCodes[errCode])
   874  		util.RespondWithJSON(w, http.StatusBadRequest,
   875  			v2.UserErrorReply{
   876  				ErrorCode: errCode,
   877  			})
   878  		return
   879  
   880  	case errors.As(err, &ue):
   881  		// Politeiad user error
   882  		m := fmt.Sprintf("%v User error: %v %v", util.RemoteAddr(r),
   883  			ue.ErrorCode, v2.ErrorCodes[ue.ErrorCode])
   884  		if ce.ErrorContext != "" {
   885  			m += fmt.Sprintf(": %v", ce.ErrorContext)
   886  		}
   887  		log.Infof(m)
   888  		util.RespondWithJSON(w, http.StatusBadRequest, ue)
   889  		return
   890  
   891  	case errors.As(err, &ce):
   892  		// Backend content error
   893  		errCode := convertContentErrorToV2(ce.ErrorCode)
   894  		m := fmt.Sprintf("%v User error: %v %v", util.RemoteAddr(r),
   895  			errCode, v2.ErrorCodes[errCode])
   896  		if ce.ErrorContext != "" {
   897  			m += fmt.Sprintf(": %v", ce.ErrorContext)
   898  		}
   899  		log.Infof(m)
   900  		util.RespondWithJSON(w, http.StatusBadRequest,
   901  			v2.UserErrorReply{
   902  				ErrorCode:    errCode,
   903  				ErrorContext: ce.ErrorContext,
   904  			})
   905  		return
   906  
   907  	case errors.As(err, &ste):
   908  		// Backend status transition error
   909  		log.Infof("%v User error: %v", util.RemoteAddr(r), ste.Error())
   910  		util.RespondWithJSON(w, http.StatusBadRequest,
   911  			v2.UserErrorReply{
   912  				ErrorCode:    v2.ErrorCodeStatusChangeInvalid,
   913  				ErrorContext: ste.Error(),
   914  			})
   915  		return
   916  
   917  	case errors.As(err, &pe):
   918  		// Plugin user error
   919  		m := fmt.Sprintf("%v Plugin error: %v %v",
   920  			util.RemoteAddr(r), pe.PluginID, pe.ErrorCode)
   921  		if pe.ErrorContext != "" {
   922  			m += fmt.Sprintf(": %v", pe.ErrorContext)
   923  		}
   924  		log.Infof(m)
   925  		util.RespondWithJSON(w, http.StatusBadRequest,
   926  			v2.PluginErrorReply{
   927  				PluginID:     pe.PluginID,
   928  				ErrorCode:    pe.ErrorCode,
   929  				ErrorContext: pe.ErrorContext,
   930  			})
   931  		return
   932  	}
   933  
   934  	// Internal server error. Log it and return a 500.
   935  	t := time.Now().Unix()
   936  	e := fmt.Sprintf(format, err)
   937  	log.Errorf("%v %v %v %v Internal error %v: %v",
   938  		util.RemoteAddr(r), r.Method, r.URL, r.Proto, t, e)
   939  	log.Errorf("Stacktrace (NOT A REAL CRASH): %s", debug.Stack())
   940  
   941  	util.RespondWithJSON(w, http.StatusInternalServerError,
   942  		v2.ServerErrorReply{
   943  			ErrorCode: t,
   944  		})
   945  }
   946  
   947  func convertErrorToV2(e error) v2.ErrorCodeT {
   948  	switch e {
   949  	case backendv2.ErrTokenInvalid:
   950  		return v2.ErrorCodeTokenInvalid
   951  	case backendv2.ErrRecordNotFound:
   952  		return v2.ErrorCodeRecordNotFound
   953  	case backendv2.ErrRecordLocked:
   954  		return v2.ErrorCodeRecordLocked
   955  	case backendv2.ErrNoRecordChanges:
   956  		return v2.ErrorCodeNoRecordChanges
   957  	case backendv2.ErrPluginIDInvalid:
   958  		return v2.ErrorCodePluginIDInvalid
   959  	case backendv2.ErrPluginCmdInvalid:
   960  		return v2.ErrorCodePluginCmdInvalid
   961  	case backendv2.ErrDuplicatePayload:
   962  		return v2.ErrorCodeDuplicatePayload
   963  	}
   964  	return v2.ErrorCodeInvalid
   965  }
   966  
   967  func convertContentErrorToV2(e backendv2.ContentErrorCodeT) v2.ErrorCodeT {
   968  	switch e {
   969  	case backendv2.ContentErrorMetadataStreamInvalid:
   970  		return v2.ErrorCodeMetadataStreamInvalid
   971  	case backendv2.ContentErrorMetadataStreamDuplicate:
   972  		return v2.ErrorCodeMetadataStreamDuplicate
   973  	case backendv2.ContentErrorFilesEmpty:
   974  		return v2.ErrorCodeFilesEmpty
   975  	case backendv2.ContentErrorFileNameInvalid:
   976  		return v2.ErrorCodeFileNameInvalid
   977  	case backendv2.ContentErrorFileNameDuplicate:
   978  		return v2.ErrorCodeFileNameDuplicate
   979  	case backendv2.ContentErrorFileDigestInvalid:
   980  		return v2.ErrorCodeFileDigestInvalid
   981  	case backendv2.ContentErrorFilePayloadInvalid:
   982  		return v2.ErrorCodeFilePayloadInvalid
   983  	case backendv2.ContentErrorFileMIMETypeInvalid:
   984  		return v2.ErrorCodeFileMIMETypeInvalid
   985  	case backendv2.ContentErrorFileMIMETypeUnsupported:
   986  		return v2.ErrorCodeFileMIMETypeUnsupported
   987  	}
   988  	return v2.ErrorCodeInvalid
   989  }