github.com/safing/portbase@v0.19.5/api/endpoints.go (about)

     1  package api
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/gorilla/mux"
    15  
    16  	"github.com/safing/portbase/database/record"
    17  	"github.com/safing/portbase/formats/dsd"
    18  	"github.com/safing/portbase/log"
    19  	"github.com/safing/portbase/modules"
    20  )
    21  
    22  // Endpoint describes an API Endpoint.
    23  // Path and at least one permission are required.
    24  // As is exactly one function.
    25  type Endpoint struct { //nolint:maligned
    26  	// Name is the human reabable name of the endpoint.
    27  	Name string
    28  	// Description is the human readable description and documentation of the endpoint.
    29  	Description string
    30  	// Parameters is the parameter documentation.
    31  	Parameters []Parameter `json:",omitempty"`
    32  
    33  	// Path describes the URL path of the endpoint.
    34  	Path string
    35  
    36  	// MimeType defines the content type of the returned data.
    37  	MimeType string
    38  
    39  	// Read defines the required read permission.
    40  	Read Permission `json:",omitempty"`
    41  
    42  	// ReadMethod sets the required read method for the endpoint.
    43  	// Available methods are:
    44  	// GET: Returns data only, no action is taken, nothing is changed.
    45  	// If omitted, defaults to GET.
    46  	//
    47  	// This field is currently being introduced and will only warn and not deny
    48  	// access if the write method does not match.
    49  	ReadMethod string `json:",omitempty"`
    50  
    51  	// Write defines the required write permission.
    52  	Write Permission `json:",omitempty"`
    53  
    54  	// WriteMethod sets the required write method for the endpoint.
    55  	// Available methods are:
    56  	// POST: Create a new resource; Change a status; Execute a function
    57  	// PUT: Update an existing resource
    58  	// DELETE: Remove an existing resource
    59  	// If omitted, defaults to POST.
    60  	//
    61  	// This field is currently being introduced and will only warn and not deny
    62  	// access if the write method does not match.
    63  	WriteMethod string `json:",omitempty"`
    64  
    65  	// BelongsTo defines which module this endpoint belongs to.
    66  	// The endpoint will not be accessible if the module is not online.
    67  	BelongsTo *modules.Module `json:"-"`
    68  
    69  	// ActionFunc is for simple actions with a return message for the user.
    70  	ActionFunc ActionFunc `json:"-"`
    71  
    72  	// DataFunc is for returning raw data that the caller for further processing.
    73  	DataFunc DataFunc `json:"-"`
    74  
    75  	// StructFunc is for returning any kind of struct.
    76  	StructFunc StructFunc `json:"-"`
    77  
    78  	// RecordFunc is for returning a database record. It will be properly locked
    79  	// and marshalled including metadata.
    80  	RecordFunc RecordFunc `json:"-"`
    81  
    82  	// HandlerFunc is the raw http handler.
    83  	HandlerFunc http.HandlerFunc `json:"-"`
    84  }
    85  
    86  // Parameter describes a parameterized variation of an endpoint.
    87  type Parameter struct {
    88  	Method      string
    89  	Field       string
    90  	Value       string
    91  	Description string
    92  }
    93  
    94  // HTTPStatusProvider is an interface for errors to provide a custom HTTP
    95  // status code.
    96  type HTTPStatusProvider interface {
    97  	HTTPStatus() int
    98  }
    99  
   100  // HTTPStatusError represents an error with an HTTP status code.
   101  type HTTPStatusError struct {
   102  	err  error
   103  	code int
   104  }
   105  
   106  // Error returns the error message.
   107  func (e *HTTPStatusError) Error() string {
   108  	return e.err.Error()
   109  }
   110  
   111  // Unwrap return the wrapped error.
   112  func (e *HTTPStatusError) Unwrap() error {
   113  	return e.err
   114  }
   115  
   116  // HTTPStatus returns the HTTP status code this error.
   117  func (e *HTTPStatusError) HTTPStatus() int {
   118  	return e.code
   119  }
   120  
   121  // ErrorWithStatus adds the HTTP status code to the error.
   122  func ErrorWithStatus(err error, code int) error {
   123  	return &HTTPStatusError{
   124  		err:  err,
   125  		code: code,
   126  	}
   127  }
   128  
   129  type (
   130  	// ActionFunc is for simple actions with a return message for the user.
   131  	ActionFunc func(ar *Request) (msg string, err error)
   132  
   133  	// DataFunc is for returning raw data that the caller for further processing.
   134  	DataFunc func(ar *Request) (data []byte, err error)
   135  
   136  	// StructFunc is for returning any kind of struct.
   137  	StructFunc func(ar *Request) (i interface{}, err error)
   138  
   139  	// RecordFunc is for returning a database record. It will be properly locked
   140  	// and marshalled including metadata.
   141  	RecordFunc func(ar *Request) (r record.Record, err error)
   142  )
   143  
   144  // MIME Types.
   145  const (
   146  	MimeTypeJSON string = "application/json"
   147  	MimeTypeText string = "text/plain"
   148  
   149  	apiV1Path = "/api/v1/"
   150  )
   151  
   152  func init() {
   153  	RegisterHandler(apiV1Path+"{endpointPath:.+}", &endpointHandler{})
   154  }
   155  
   156  var (
   157  	endpoints     = make(map[string]*Endpoint)
   158  	endpointsMux  = mux.NewRouter()
   159  	endpointsLock sync.RWMutex
   160  
   161  	// ErrInvalidEndpoint is returned when an invalid endpoint is registered.
   162  	ErrInvalidEndpoint = errors.New("endpoint is invalid")
   163  
   164  	// ErrAlreadyRegistered is returned when there already is an endpoint with
   165  	// the same path registered.
   166  	ErrAlreadyRegistered = errors.New("an endpoint for this path is already registered")
   167  )
   168  
   169  func getAPIContext(r *http.Request) (apiEndpoint *Endpoint, apiRequest *Request) {
   170  	// Get request context and check if we already have an action cached.
   171  	apiRequest = GetAPIRequest(r)
   172  	if apiRequest == nil {
   173  		return nil, nil
   174  	}
   175  	var ok bool
   176  	apiEndpoint, ok = apiRequest.HandlerCache.(*Endpoint)
   177  	if ok {
   178  		return apiEndpoint, apiRequest
   179  	}
   180  
   181  	endpointsLock.RLock()
   182  	defer endpointsLock.RUnlock()
   183  
   184  	// Get handler for request.
   185  	// Gorilla does not support handling this on our own very well.
   186  	// See github.com/gorilla/mux.ServeHTTP for reference.
   187  	var match mux.RouteMatch
   188  	var handler http.Handler
   189  	if endpointsMux.Match(r, &match) {
   190  		handler = match.Handler
   191  		apiRequest.Route = match.Route
   192  		// Add/Override variables instead of replacing.
   193  		for k, v := range match.Vars {
   194  			apiRequest.URLVars[k] = v
   195  		}
   196  	} else {
   197  		return nil, apiRequest
   198  	}
   199  
   200  	apiEndpoint, ok = handler.(*Endpoint)
   201  	if ok {
   202  		// Cache for next operation.
   203  		apiRequest.HandlerCache = apiEndpoint
   204  	}
   205  	return apiEndpoint, apiRequest
   206  }
   207  
   208  // RegisterEndpoint registers a new endpoint. An error will be returned if it
   209  // does not pass the sanity checks.
   210  func RegisterEndpoint(e Endpoint) error {
   211  	if err := e.check(); err != nil {
   212  		return fmt.Errorf("%w: %w", ErrInvalidEndpoint, err)
   213  	}
   214  
   215  	endpointsLock.Lock()
   216  	defer endpointsLock.Unlock()
   217  
   218  	_, ok := endpoints[e.Path]
   219  	if ok {
   220  		return ErrAlreadyRegistered
   221  	}
   222  
   223  	endpoints[e.Path] = &e
   224  	endpointsMux.Handle(apiV1Path+e.Path, &e)
   225  	return nil
   226  }
   227  
   228  // GetEndpointByPath returns the endpoint registered with the given path.
   229  func GetEndpointByPath(path string) (*Endpoint, error) {
   230  	endpointsLock.Lock()
   231  	defer endpointsLock.Unlock()
   232  	endpoint, ok := endpoints[path]
   233  	if !ok {
   234  		return nil, fmt.Errorf("no registered endpoint on path: %q", path)
   235  	}
   236  
   237  	return endpoint, nil
   238  }
   239  
   240  func (e *Endpoint) check() error {
   241  	// Check path.
   242  	if strings.TrimSpace(e.Path) == "" {
   243  		return errors.New("path is missing")
   244  	}
   245  
   246  	// Check permissions.
   247  	if e.Read < Dynamic || e.Read > PermitSelf {
   248  		return errors.New("invalid read permission")
   249  	}
   250  	if e.Write < Dynamic || e.Write > PermitSelf {
   251  		return errors.New("invalid write permission")
   252  	}
   253  
   254  	// Check methods.
   255  	if e.Read != NotSupported {
   256  		switch e.ReadMethod {
   257  		case http.MethodGet:
   258  			// All good.
   259  		case "":
   260  			// Set to default.
   261  			e.ReadMethod = http.MethodGet
   262  		default:
   263  			return errors.New("invalid read method")
   264  		}
   265  	} else {
   266  		e.ReadMethod = ""
   267  	}
   268  	if e.Write != NotSupported {
   269  		switch e.WriteMethod {
   270  		case http.MethodPost,
   271  			http.MethodPut,
   272  			http.MethodDelete:
   273  			// All good.
   274  		case "":
   275  			// Set to default.
   276  			e.WriteMethod = http.MethodPost
   277  		default:
   278  			return errors.New("invalid write method")
   279  		}
   280  	} else {
   281  		e.WriteMethod = ""
   282  	}
   283  
   284  	// Check functions.
   285  	var defaultMimeType string
   286  	fnCnt := 0
   287  	if e.ActionFunc != nil {
   288  		fnCnt++
   289  		defaultMimeType = MimeTypeText
   290  	}
   291  	if e.DataFunc != nil {
   292  		fnCnt++
   293  		defaultMimeType = MimeTypeText
   294  	}
   295  	if e.StructFunc != nil {
   296  		fnCnt++
   297  		defaultMimeType = MimeTypeJSON
   298  	}
   299  	if e.RecordFunc != nil {
   300  		fnCnt++
   301  		defaultMimeType = MimeTypeJSON
   302  	}
   303  	if e.HandlerFunc != nil {
   304  		fnCnt++
   305  		defaultMimeType = MimeTypeText
   306  	}
   307  	if fnCnt != 1 {
   308  		return errors.New("only one function may be set")
   309  	}
   310  
   311  	// Set default mime type.
   312  	if e.MimeType == "" {
   313  		e.MimeType = defaultMimeType
   314  	}
   315  
   316  	return nil
   317  }
   318  
   319  // ExportEndpoints exports the registered endpoints. The returned data must be
   320  // treated as immutable.
   321  func ExportEndpoints() []*Endpoint {
   322  	endpointsLock.RLock()
   323  	defer endpointsLock.RUnlock()
   324  
   325  	// Copy the map into a slice.
   326  	eps := make([]*Endpoint, 0, len(endpoints))
   327  	for _, ep := range endpoints {
   328  		eps = append(eps, ep)
   329  	}
   330  
   331  	sort.Sort(sortByPath(eps))
   332  	return eps
   333  }
   334  
   335  type sortByPath []*Endpoint
   336  
   337  func (eps sortByPath) Len() int           { return len(eps) }
   338  func (eps sortByPath) Less(i, j int) bool { return eps[i].Path < eps[j].Path }
   339  func (eps sortByPath) Swap(i, j int)      { eps[i], eps[j] = eps[j], eps[i] }
   340  
   341  type endpointHandler struct{}
   342  
   343  var _ AuthenticatedHandler = &endpointHandler{} // Compile time interface check.
   344  
   345  // ReadPermission returns the read permission for the handler.
   346  func (eh *endpointHandler) ReadPermission(r *http.Request) Permission {
   347  	apiEndpoint, _ := getAPIContext(r)
   348  	if apiEndpoint != nil {
   349  		return apiEndpoint.Read
   350  	}
   351  	return NotFound
   352  }
   353  
   354  // WritePermission returns the write permission for the handler.
   355  func (eh *endpointHandler) WritePermission(r *http.Request) Permission {
   356  	apiEndpoint, _ := getAPIContext(r)
   357  	if apiEndpoint != nil {
   358  		return apiEndpoint.Write
   359  	}
   360  	return NotFound
   361  }
   362  
   363  // ServeHTTP handles the http request.
   364  func (eh *endpointHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   365  	apiEndpoint, apiRequest := getAPIContext(r)
   366  	if apiEndpoint == nil || apiRequest == nil {
   367  		http.NotFound(w, r)
   368  		return
   369  	}
   370  
   371  	apiEndpoint.ServeHTTP(w, r)
   372  }
   373  
   374  // ServeHTTP handles the http request.
   375  func (e *Endpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   376  	_, apiRequest := getAPIContext(r)
   377  	if apiRequest == nil {
   378  		http.NotFound(w, r)
   379  		return
   380  	}
   381  
   382  	// Wait for the owning module to be ready.
   383  	if !moduleIsReady(e.BelongsTo) {
   384  		http.Error(w, "The API endpoint is not ready yet or the its module is not enabled. Reload (F5) to try again.", http.StatusServiceUnavailable)
   385  		return
   386  	}
   387  
   388  	// Return OPTIONS request before starting to handle normal requests.
   389  	if r.Method == http.MethodOptions {
   390  		w.WriteHeader(http.StatusNoContent)
   391  		return
   392  	}
   393  
   394  	eMethod, readMethod, ok := getEffectiveMethod(r)
   395  	if !ok {
   396  		http.Error(w, "unsupported method for the actions API", http.StatusMethodNotAllowed)
   397  		return
   398  	}
   399  
   400  	if readMethod {
   401  		if eMethod != e.ReadMethod {
   402  			log.Tracer(r.Context()).Warningf(
   403  				"api: method %q does not match required read method %q%s",
   404  				r.Method,
   405  				e.ReadMethod,
   406  				" - this will be an error and abort the request in the future",
   407  			)
   408  		}
   409  	} else {
   410  		if eMethod != e.WriteMethod {
   411  			log.Tracer(r.Context()).Warningf(
   412  				"api: method %q does not match required write method %q%s",
   413  				r.Method,
   414  				e.WriteMethod,
   415  				" - this will be an error and abort the request in the future",
   416  			)
   417  		}
   418  	}
   419  
   420  	switch eMethod {
   421  	case http.MethodGet, http.MethodDelete:
   422  		// Nothing to do for these.
   423  	case http.MethodPost, http.MethodPut:
   424  		// Read body data.
   425  		inputData, ok := readBody(w, r)
   426  		if !ok {
   427  			return
   428  		}
   429  		apiRequest.InputData = inputData
   430  
   431  		// restore request body for any http.HandlerFunc below
   432  		r.Body = io.NopCloser(bytes.NewReader(inputData))
   433  	default:
   434  		// Defensive.
   435  		http.Error(w, "unsupported method for the actions API", http.StatusMethodNotAllowed)
   436  		return
   437  	}
   438  
   439  	// Add response headers to request struct so that the endpoint can work with them.
   440  	apiRequest.ResponseHeader = w.Header()
   441  
   442  	// Execute action function and get response data
   443  	var responseData []byte
   444  	var err error
   445  
   446  	switch {
   447  	case e.ActionFunc != nil:
   448  		var msg string
   449  		msg, err = e.ActionFunc(apiRequest)
   450  		if !strings.HasSuffix(msg, "\n") {
   451  			msg += "\n"
   452  		}
   453  		if err == nil {
   454  			responseData = []byte(msg)
   455  		}
   456  
   457  	case e.DataFunc != nil:
   458  		responseData, err = e.DataFunc(apiRequest)
   459  
   460  	case e.StructFunc != nil:
   461  		var v interface{}
   462  		v, err = e.StructFunc(apiRequest)
   463  		if err == nil && v != nil {
   464  			var mimeType string
   465  			responseData, mimeType, _, err = dsd.MimeDump(v, r.Header.Get("Accept"))
   466  			if err == nil {
   467  				w.Header().Set("Content-Type", mimeType)
   468  			}
   469  		}
   470  
   471  	case e.RecordFunc != nil:
   472  		var rec record.Record
   473  		rec, err = e.RecordFunc(apiRequest)
   474  		if err == nil && r != nil {
   475  			responseData, err = MarshalRecord(rec, false)
   476  		}
   477  
   478  	case e.HandlerFunc != nil:
   479  		e.HandlerFunc(w, r)
   480  		return
   481  
   482  	default:
   483  		http.Error(w, "missing handler", http.StatusInternalServerError)
   484  		return
   485  	}
   486  
   487  	// Check for handler error.
   488  	if err != nil {
   489  		var statusProvider HTTPStatusProvider
   490  		if errors.As(err, &statusProvider) {
   491  			http.Error(w, err.Error(), statusProvider.HTTPStatus())
   492  		} else {
   493  			http.Error(w, err.Error(), http.StatusInternalServerError)
   494  		}
   495  		return
   496  	}
   497  
   498  	// Return no content if there is none, or if request is HEAD.
   499  	if len(responseData) == 0 || r.Method == http.MethodHead {
   500  		w.WriteHeader(http.StatusNoContent)
   501  		return
   502  	}
   503  
   504  	// Set content type if not yet set.
   505  	if w.Header().Get("Content-Type") == "" {
   506  		w.Header().Set("Content-Type", e.MimeType+"; charset=utf-8")
   507  	}
   508  
   509  	// Write response.
   510  	w.Header().Set("Content-Length", strconv.Itoa(len(responseData)))
   511  	w.WriteHeader(http.StatusOK)
   512  	_, err = w.Write(responseData)
   513  	if err != nil {
   514  		log.Tracer(r.Context()).Warningf("api: failed to write response: %s", err)
   515  	}
   516  }
   517  
   518  func readBody(w http.ResponseWriter, r *http.Request) (inputData []byte, ok bool) {
   519  	// Check for too long content in order to prevent death.
   520  	if r.ContentLength > 20000000 { // 20MB
   521  		http.Error(w, "too much input data", http.StatusRequestEntityTooLarge)
   522  		return nil, false
   523  	}
   524  
   525  	// Read and close body.
   526  	inputData, err := io.ReadAll(r.Body)
   527  	if err != nil {
   528  		http.Error(w, "failed to read body"+err.Error(), http.StatusInternalServerError)
   529  		return nil, false
   530  	}
   531  	return inputData, true
   532  }