github.com/decred/politeia@v1.4.0/politeiawww/handlers.go (about)

     1  // Copyright (c) 2022 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  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"net/http"
    12  	"net/http/httputil"
    13  	"runtime/debug"
    14  	"time"
    15  
    16  	v3 "github.com/decred/politeia/politeiawww/api/http/v3"
    17  	"github.com/decred/politeia/politeiawww/logger"
    18  	plugin "github.com/decred/politeia/politeiawww/plugin/v1"
    19  	"github.com/decred/politeia/util"
    20  	"github.com/gorilla/csrf"
    21  	"github.com/pkg/errors"
    22  )
    23  
    24  // handleNotFound handles all invalid routes and returns a 404 to the client.
    25  func (p *politeiawww) handleNotFound(w http.ResponseWriter, r *http.Request) {
    26  	// Log incoming connection
    27  	log.Debugf("Invalid route: %v %v %v %v",
    28  		util.RemoteAddr(r), r.Method, r.URL, r.Proto)
    29  
    30  	// Trace incoming request
    31  	log.Tracef("%v", logger.NewLogClosure(func() string {
    32  		trace, err := httputil.DumpRequest(r, true)
    33  		if err != nil {
    34  			trace = []byte(fmt.Sprintf("handleNotFound: DumpRequest %v", err))
    35  		}
    36  		return string(trace)
    37  	}))
    38  
    39  	util.RespondWithJSON(w, http.StatusNotFound, nil)
    40  }
    41  
    42  // handleVersion is the request handler for the http v3 VersionRoute.
    43  func (p *politeiawww) handleVersion(w http.ResponseWriter, r *http.Request) {
    44  	log.Tracef("handleVersion")
    45  
    46  	// Set the CSRF header. This is the only route
    47  	// that sets the CSRF header.
    48  	w.Header().Set(v3.CSRFTokenHeader, csrf.Token(r))
    49  
    50  	plugins := make(map[string]uint32, len(p.plugins))
    51  	for _, plugin := range p.plugins {
    52  		plugins[plugin.ID()] = plugin.Version()
    53  	}
    54  
    55  	util.RespondWithJSON(w, http.StatusOK,
    56  		v3.VersionReply{
    57  			APIVersion:   v3.APIVersion,
    58  			BuildVersion: p.cfg.Version,
    59  			Plugins:      plugins,
    60  		})
    61  }
    62  
    63  // handlePolicy is the request handler for the http v3 PolicyRoute.
    64  func (p *politeiawww) handlePolicy(w http.ResponseWriter, r *http.Request) {
    65  	log.Tracef("handlePolicy")
    66  
    67  	util.RespondWithJSON(w, http.StatusOK,
    68  		v3.PolicyReply{
    69  			ReadBatchLimit: p.cfg.PluginBatchLimit,
    70  		})
    71  }
    72  
    73  // handleNewUser is the request handler for the http v3 NewUserRoute.
    74  func (p *politeiawww) handleNewUser(w http.ResponseWriter, r *http.Request) {
    75  	log.Tracef("handleNewUser")
    76  
    77  	// Decode the request body
    78  	var cmd v3.Cmd
    79  	decoder := json.NewDecoder(r.Body)
    80  	if err := decoder.Decode(&cmd); err != nil {
    81  		util.RespondWithJSON(w, http.StatusOK,
    82  			v3.CmdReply{
    83  				Error: v3.UserError{
    84  					ErrorCode: v3.ErrorCodeInvalidInput,
    85  				},
    86  			})
    87  		return
    88  	}
    89  
    90  	// Verify the plugin is the user plugin
    91  	if p.userManager.ID() != cmd.PluginID {
    92  		util.RespondWithJSON(w, http.StatusOK,
    93  			v3.CmdReply{
    94  				Error: v3.UserError{
    95  					ErrorCode: v3.ErrorCodePluginNotAuthorized,
    96  				},
    97  			})
    98  		return
    99  	}
   100  
   101  	// Extract the session data from the request cookies
   102  	s, err := p.extractSession(r)
   103  	if err != nil {
   104  		respondWithError(w, r,
   105  			"handleNewUser: extractSession: %v", err)
   106  		return
   107  	}
   108  
   109  	// Execute the plugin command
   110  	var (
   111  		pluginSession = convertSession(s)
   112  		pluginCmd     = convertCmdFromHTTP(cmd)
   113  	)
   114  	pluginReply, err := p.newUserCmd(r.Context(),
   115  		pluginSession, pluginCmd)
   116  	if err != nil {
   117  		respondWithError(w, r,
   118  			"handleNewUser: pluginNewUser: %v", err)
   119  		return
   120  	}
   121  
   122  	reply := convertReplyToHTTP(pluginCmd, *pluginReply)
   123  
   124  	// Save any updates that were made to the user session
   125  	err = p.saveUserSession(r, w, s, pluginSession)
   126  	if err != nil {
   127  		// The plugin command has already been executed.
   128  		// Handled the error gracefully.
   129  		log.Errorf("handleNewUser: saveSession: %v", err)
   130  	}
   131  
   132  	// Send the response
   133  	util.RespondWithJSON(w, http.StatusOK, reply)
   134  }
   135  
   136  // handleWrite is the request handler for the http v3 WriteRoute.
   137  func (p *politeiawww) handleWrite(w http.ResponseWriter, r *http.Request) {
   138  	log.Tracef("handleWrite")
   139  
   140  	// Decode the request body
   141  	var cmd v3.Cmd
   142  	decoder := json.NewDecoder(r.Body)
   143  	if err := decoder.Decode(&cmd); err != nil {
   144  		util.RespondWithJSON(w, http.StatusOK,
   145  			v3.CmdReply{
   146  				Error: v3.UserError{
   147  					ErrorCode: v3.ErrorCodeInvalidInput,
   148  				},
   149  			})
   150  		return
   151  	}
   152  
   153  	// Verify the plugin exists
   154  	_, ok := p.plugins[cmd.PluginID]
   155  	if !ok {
   156  		util.RespondWithJSON(w, http.StatusOK,
   157  			v3.CmdReply{
   158  				Error: v3.UserError{
   159  					ErrorCode: v3.ErrorCodePluginNotFound,
   160  				},
   161  			})
   162  		return
   163  	}
   164  
   165  	// Extract the session data from the request cookies
   166  	s, err := p.extractSession(r)
   167  	if err != nil {
   168  		respondWithError(w, r,
   169  			"handleWrite: extractSession: %v", err)
   170  		return
   171  	}
   172  
   173  	// Execute the plugin command
   174  	var (
   175  		pluginSession = convertSession(s)
   176  		pluginCmd     = convertCmdFromHTTP(cmd)
   177  	)
   178  	pluginReply, err := p.writeCmd(r.Context(), pluginSession, pluginCmd)
   179  	if err != nil {
   180  		respondWithError(w, r,
   181  			"handleWrite: pluginWrite: %v", err)
   182  		return
   183  	}
   184  
   185  	reply := convertReplyToHTTP(pluginCmd, *pluginReply)
   186  
   187  	// Save any updates that were made to the user session
   188  	err = p.saveUserSession(r, w, s, pluginSession)
   189  	if err != nil {
   190  		// The plugin command has already been executed.
   191  		// Handled the error gracefully.
   192  		log.Errorf("handleWrite: saveSession: %v", err)
   193  	}
   194  
   195  	// Send the response
   196  	util.RespondWithJSON(w, http.StatusOK, reply)
   197  }
   198  
   199  // handleRead is the request handler for the http v3 ReadRoute.
   200  func (p *politeiawww) handleRead(w http.ResponseWriter, r *http.Request) {
   201  	log.Tracef("handleRead")
   202  
   203  	// Decode the request body
   204  	var cmd v3.Cmd
   205  	decoder := json.NewDecoder(r.Body)
   206  	if err := decoder.Decode(&cmd); err != nil {
   207  		util.RespondWithJSON(w, http.StatusOK,
   208  			v3.CmdReply{
   209  				Error: v3.UserError{
   210  					ErrorCode: v3.ErrorCodeInvalidInput,
   211  				},
   212  			})
   213  		return
   214  	}
   215  
   216  	// Verify plugin exists
   217  	_, ok := p.plugins[cmd.PluginID]
   218  	if !ok {
   219  		util.RespondWithJSON(w, http.StatusOK,
   220  			v3.CmdReply{
   221  				Error: v3.UserError{
   222  					ErrorCode: v3.ErrorCodePluginNotFound,
   223  				},
   224  			})
   225  		return
   226  	}
   227  
   228  	// Extract the session data from the request cookies
   229  	s, err := p.extractSession(r)
   230  	if err != nil {
   231  		respondWithError(w, r,
   232  			"handleRead: extractSession: %v", err)
   233  		return
   234  	}
   235  
   236  	// Execute the plugin command
   237  	var (
   238  		pluginSession = convertSession(s)
   239  		pluginCmd     = convertCmdFromHTTP(cmd)
   240  	)
   241  	pluginReply, err := p.readCmd(r.Context(), pluginSession, pluginCmd)
   242  	if err != nil {
   243  		respondWithError(w, r,
   244  			"handleRead: readCmd: %v", err)
   245  		return
   246  	}
   247  
   248  	reply := convertReplyToHTTP(pluginCmd, *pluginReply)
   249  
   250  	// Save any updates that were made to the user session
   251  	err = p.saveUserSession(r, w, s, pluginSession)
   252  	if err != nil {
   253  		// The plugin command has already been executed.
   254  		// Handle the error gracefully.
   255  		log.Errorf("handleRead: saveSession: %v", err)
   256  	}
   257  
   258  	// Send the response
   259  	util.RespondWithJSON(w, http.StatusOK, reply)
   260  }
   261  
   262  // handleReadBatch is the request handler for the http v3 ReadBatchRoute.
   263  func (p *politeiawww) handleReadBatch(w http.ResponseWriter, r *http.Request) {
   264  	log.Tracef("handleReadBatch")
   265  
   266  	// Decode the request body
   267  	var batch v3.Batch
   268  	decoder := json.NewDecoder(r.Body)
   269  	if err := decoder.Decode(&batch); err != nil {
   270  		util.RespondWithJSON(w, http.StatusOK,
   271  			v3.CmdReply{
   272  				Error: v3.UserError{
   273  					ErrorCode: v3.ErrorCodeInvalidInput,
   274  				},
   275  			})
   276  		return
   277  	}
   278  	if len(batch.Cmds) > int(p.cfg.PluginBatchLimit) {
   279  		util.RespondWithJSON(w, http.StatusOK,
   280  			v3.CmdReply{
   281  				Error: v3.UserError{
   282  					ErrorCode: v3.ErrorCodeBatchLimitExceeded,
   283  					ErrorContext: fmt.Sprintf("max number of cmds is %v",
   284  						p.cfg.PluginBatchLimit),
   285  				},
   286  			})
   287  		return
   288  	}
   289  
   290  	// Extract the session data from the request cookies
   291  	s, err := p.extractSession(r)
   292  	if err != nil {
   293  		respondWithError(w, r,
   294  			"handleReadBatch: extractSession: %v", err)
   295  		return
   296  	}
   297  
   298  	var (
   299  		pluginSession = convertSession(s)
   300  		replies       = make([]v3.CmdReply, len(batch.Cmds))
   301  	)
   302  	for i, cmd := range batch.Cmds {
   303  		// Verify plugin exists
   304  		_, ok := p.plugins[cmd.PluginID]
   305  		if !ok {
   306  			replies[i] = v3.CmdReply{
   307  				Error: v3.UserError{
   308  					ErrorCode: v3.ErrorCodePluginNotFound,
   309  				},
   310  			}
   311  			continue
   312  		}
   313  
   314  		// Execute the plugin command
   315  		pluginCmd := convertCmdFromHTTP(cmd)
   316  		pluginReply, err := p.readCmd(r.Context(), pluginSession, pluginCmd)
   317  		if err != nil {
   318  			respondWithError(w, r,
   319  				"handleReadBatch: readCmd: %v", err)
   320  			return
   321  		}
   322  
   323  		replies[i] = convertReplyToHTTP(pluginCmd, *pluginReply)
   324  	}
   325  
   326  	// Save any updates that were made to the user session
   327  	err = p.saveUserSession(r, w, s, pluginSession)
   328  	if err != nil {
   329  		// The plugin command has already been executed. Handle
   330  		// the error gracefully.
   331  		log.Errorf("handleReadBatch: saveSession: %v", err)
   332  	}
   333  
   334  	// Send the response
   335  	util.RespondWithJSON(w, http.StatusOK,
   336  		v3.BatchReply{
   337  			Replies: replies,
   338  		})
   339  }
   340  
   341  // responseWithError checks the error type and responds with the appropriate
   342  // HTTP error response.
   343  func respondWithError(w http.ResponseWriter, r *http.Request, format string, err error) {
   344  	// Check if the client dropped the connection
   345  	if err := r.Context().Err(); err == context.Canceled {
   346  		log.Infof("%v %v %v %v client aborted connection",
   347  			util.RemoteAddr(r), r.Method, r.URL, r.Proto)
   348  
   349  		// The client dropped the connection. There
   350  		// is no need to send a response.
   351  		return
   352  	}
   353  
   354  	// Check if this a user error
   355  	var ue v3.UserError
   356  	if errors.As(err, &ue) {
   357  		m := fmt.Sprintf("%v User error: %v %v",
   358  			util.RemoteAddr(r), ue.ErrorCode, v3.ErrorCodes[ue.ErrorCode])
   359  		if ue.ErrorContext != "" {
   360  			m += fmt.Sprintf(": %v", ue.ErrorContext)
   361  		}
   362  		log.Infof(m)
   363  
   364  		util.RespondWithJSON(w, http.StatusOK,
   365  			v3.CmdReply{
   366  				Error: ue,
   367  			})
   368  		return
   369  	}
   370  
   371  	// This is an internal server error. Log it and return a 500.
   372  	t := time.Now().Unix()
   373  	e := fmt.Sprintf(format, err)
   374  	log.Errorf("%v %v %v %v Internal error %v: %v",
   375  		util.RemoteAddr(r), r.Method, r.URL, r.Proto, t, e)
   376  
   377  	// If this is a pkg/errors error then we can pull the
   378  	// stack trace out of the error, otherwise, we use the
   379  	// stack trace that points to this function.
   380  	stack, ok := util.StackTrace(err)
   381  	if !ok {
   382  		stack = string(debug.Stack())
   383  	}
   384  
   385  	log.Errorf("Stacktrace (NOT A REAL CRASH): %v", stack)
   386  
   387  	util.RespondWithJSON(w, http.StatusInternalServerError,
   388  		v3.InternalError{
   389  			ErrorCode: t,
   390  		})
   391  }
   392  
   393  // convertCmdFromHTTP converts a http v3 Cmd to a plugin Cmd.
   394  func convertCmdFromHTTP(c v3.Cmd) plugin.Cmd {
   395  	return plugin.Cmd{
   396  		PluginID: c.PluginID,
   397  		Version:  c.Version,
   398  		Cmd:      c.Cmd,
   399  		Payload:  c.Payload,
   400  	}
   401  }
   402  
   403  // convertCmdFromHTTP converts a plugin Reply to a http v3 CmdReply.
   404  func convertReplyToHTTP(c plugin.Cmd, r plugin.Reply) v3.CmdReply {
   405  	return v3.CmdReply{
   406  		PluginID: c.PluginID,
   407  		Version:  c.Version,
   408  		Cmd:      c.Cmd,
   409  		Payload:  r.Payload,
   410  		Error:    r.Error,
   411  	}
   412  }