mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 05:19:39 +02:00
feat(libhttp): move into the Portainer repository EE-5475 (#10231)
This commit is contained in:
parent
090fa4aeb3
commit
8cc5e0796c
249 changed files with 1059 additions and 639 deletions
17
pkg/libhttp/LICENSE
Normal file
17
pkg/libhttp/LICENSE
Normal file
|
@ -0,0 +1,17 @@
|
|||
Copyright (c) 2018-2020 Portainer.io
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
5
pkg/libhttp/README.md
Normal file
5
pkg/libhttp/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
[](https://godoc.org/github.com/portainer/libhttp)
|
||||
|
||||
# libhttp
|
||||
|
||||
A HTTP library providing useful methods when working with `net/http` and `gorilla/mux`.
|
46
pkg/libhttp/error/error.go
Normal file
46
pkg/libhttp/error/error.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Package error provides error/logging functions that can be used in conjuction with http.Handler.
|
||||
package error
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type (
|
||||
// LoggerHandler defines a HTTP handler that includes a HandlerError return pointer
|
||||
LoggerHandler func(http.ResponseWriter, *http.Request) *HandlerError
|
||||
|
||||
errorResponse struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func (handler LoggerHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
err := handler(rw, r)
|
||||
if err != nil {
|
||||
writeErrorResponse(rw, err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeErrorResponse(rw http.ResponseWriter, err *HandlerError) {
|
||||
if err.Err == nil {
|
||||
err.Err = errors.New(err.Message)
|
||||
}
|
||||
|
||||
log.Debug().CallerSkipFrame(2).Err(err.Err).Int("status_code", err.StatusCode).Str("msg", err.Message).Msg("HTTP error")
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.WriteHeader(err.StatusCode)
|
||||
|
||||
json.NewEncoder(rw).Encode(&errorResponse{Message: err.Message, Details: err.Err.Error()})
|
||||
}
|
||||
|
||||
// WriteError is a convenience function that creates a new HandlerError before calling writeErrorResponse.
|
||||
// For use outside of the standard http handlers.
|
||||
func WriteError(rw http.ResponseWriter, code int, message string, err error) {
|
||||
writeErrorResponse(rw, &HandlerError{code, message, err})
|
||||
}
|
42
pkg/libhttp/error/status.go
Normal file
42
pkg/libhttp/error/status.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package error
|
||||
|
||||
import "net/http"
|
||||
|
||||
// HandlerError represents an error raised inside a HTTP handler
|
||||
type HandlerError struct {
|
||||
StatusCode int
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (h *HandlerError) Error() string {
|
||||
return h.Message
|
||||
}
|
||||
|
||||
func NewError(statusCode int, message string, err error) *HandlerError {
|
||||
return &HandlerError{
|
||||
StatusCode: statusCode,
|
||||
Message: message,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func BadRequest(message string, err error) *HandlerError {
|
||||
return NewError(http.StatusBadRequest, message, err)
|
||||
}
|
||||
|
||||
func NotFound(message string, err error) *HandlerError {
|
||||
return NewError(http.StatusNotFound, message, err)
|
||||
}
|
||||
|
||||
func InternalServerError(message string, err error) *HandlerError {
|
||||
return NewError(http.StatusInternalServerError, message, err)
|
||||
}
|
||||
|
||||
func Unauthorized(message string, err error) *HandlerError {
|
||||
return NewError(http.StatusUnauthorized, message, err)
|
||||
}
|
||||
|
||||
func Forbidden(message string, err error) *HandlerError {
|
||||
return NewError(http.StatusForbidden, message, err)
|
||||
}
|
37
pkg/libhttp/request/payload.go
Normal file
37
pkg/libhttp/request/payload.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package request
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PayloadValidation is an interface used to validate the payload of a request.
|
||||
type PayloadValidation interface {
|
||||
Validate(request *http.Request) error
|
||||
}
|
||||
|
||||
// DecodeAndValidateJSONPayload decodes the body of the request into an object
|
||||
// implementing the PayloadValidation interface.
|
||||
// It also triggers a validation of object content.
|
||||
func DecodeAndValidateJSONPayload(request *http.Request, v PayloadValidation) error {
|
||||
if err := json.NewDecoder(request.Body).Decode(v); err != nil {
|
||||
return err
|
||||
}
|
||||
return v.Validate(request)
|
||||
}
|
||||
|
||||
// GetPayload decodes the body of the request into an object implementing the PayloadValidation interface.
|
||||
func GetPayload[T any, PT interface {
|
||||
*T
|
||||
Validate(request *http.Request) error
|
||||
}](r *http.Request) (PT, error) {
|
||||
p := PT(new(T))
|
||||
|
||||
err := DecodeAndValidateJSONPayload(r, p)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "Invalid request payload")
|
||||
}
|
||||
return p, nil
|
||||
}
|
43
pkg/libhttp/request/payload_test.go
Normal file
43
pkg/libhttp/request/payload_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package request_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type requestPayload struct {
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
}
|
||||
|
||||
func (p *requestPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_GetPayload(t *testing.T) {
|
||||
|
||||
payload := requestPayload{
|
||||
FirstName: "John",
|
||||
LastName: "Doe",
|
||||
}
|
||||
|
||||
payloadJSON, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(payloadJSON))
|
||||
newPayload, err := request.GetPayload[requestPayload](r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, payload, *newPayload)
|
||||
}
|
152
pkg/libhttp/request/request.go
Normal file
152
pkg/libhttp/request/request.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
// Package request provides function to retrieve content from a *http.Request object of the net/http standard library,
|
||||
// be it JSON body payload, multi-part form values or query parameters.
|
||||
// It also provides functions to retrieve route variables when using gorilla/mux.
|
||||
package request
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrInvalidQueryParameter defines the message of an error raised when a mandatory query parameter has an invalid value.
|
||||
ErrInvalidQueryParameter = "Invalid query parameter"
|
||||
// ErrInvalidRequestURL defines the message of an error raised when the data sent in the query or the URL is invalid
|
||||
ErrInvalidRequestURL = "Invalid request URL"
|
||||
// ErrMissingQueryParameter defines the message of an error raised when a mandatory query parameter is missing.
|
||||
ErrMissingQueryParameter = "Missing query parameter"
|
||||
// ErrMissingFormDataValue defines the message of an error raised when a mandatory form data value is missing.
|
||||
ErrMissingFormDataValue = "Missing form data value"
|
||||
)
|
||||
|
||||
// RetrieveMultiPartFormFile returns the content of an uploaded file (form data) as bytes as well
|
||||
// as the name of the uploaded file.
|
||||
func RetrieveMultiPartFormFile(request *http.Request, requestParameter string) ([]byte, string, error) {
|
||||
file, headers, err := request.FormFile(requestParameter)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileContent, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return fileContent, headers.Filename, nil
|
||||
}
|
||||
|
||||
// RetrieveMultiPartFormJSONValue decodes the value of some form data as a JSON object into the target parameter.
|
||||
// If optional is set to true, will not return an error when the form data value is not found.
|
||||
func RetrieveMultiPartFormJSONValue(request *http.Request, name string, target interface{}, optional bool) error {
|
||||
value, err := RetrieveMultiPartFormValue(request, name, optional)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal([]byte(value), target)
|
||||
}
|
||||
|
||||
// RetrieveMultiPartFormValue returns the value of some form data as a string.
|
||||
// If optional is set to true, will not return an error when the form data value is not found.
|
||||
func RetrieveMultiPartFormValue(request *http.Request, name string, optional bool) (string, error) {
|
||||
value := request.FormValue(name)
|
||||
if value == "" && !optional {
|
||||
return "", errors.New(ErrMissingFormDataValue)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// RetrieveNumericMultiPartFormValue returns the value of some form data as an integer.
|
||||
// If optional is set to true, will not return an error when the form data value is not found.
|
||||
func RetrieveNumericMultiPartFormValue(request *http.Request, name string, optional bool) (int, error) {
|
||||
value, err := RetrieveMultiPartFormValue(request, name, optional)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(value)
|
||||
}
|
||||
|
||||
// RetrieveBooleanMultiPartFormValue returns the value of some form data as a boolean.
|
||||
// If optional is set to true, will not return an error when the form data value is not found.
|
||||
func RetrieveBooleanMultiPartFormValue(request *http.Request, name string, optional bool) (bool, error) {
|
||||
value, err := RetrieveMultiPartFormValue(request, name, optional)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return value == "true", nil
|
||||
}
|
||||
|
||||
// RetrieveRouteVariableValue returns the value of a route variable as a string.
|
||||
func RetrieveRouteVariableValue(request *http.Request, name string) (string, error) {
|
||||
routeVariables := mux.Vars(request)
|
||||
if routeVariables == nil {
|
||||
return "", errors.New(ErrInvalidRequestURL)
|
||||
}
|
||||
routeVar := routeVariables[name]
|
||||
if routeVar == "" {
|
||||
return "", errors.New(ErrInvalidRequestURL)
|
||||
}
|
||||
return routeVar, nil
|
||||
}
|
||||
|
||||
// RetrieveNumericRouteVariableValue returns the value of a route variable as an integer.
|
||||
func RetrieveNumericRouteVariableValue(request *http.Request, name string) (int, error) {
|
||||
routeVar, err := RetrieveRouteVariableValue(request, name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(routeVar)
|
||||
}
|
||||
|
||||
// RetrieveQueryParameter returns the value of a query parameter as a string.
|
||||
// If optional is set to true, will not return an error when the query parameter is not found.
|
||||
func RetrieveQueryParameter(request *http.Request, name string, optional bool) (string, error) {
|
||||
queryParameter := request.FormValue(name)
|
||||
if queryParameter == "" && !optional {
|
||||
return "", errors.New(ErrMissingQueryParameter)
|
||||
}
|
||||
return queryParameter, nil
|
||||
}
|
||||
|
||||
// RetrieveNumericQueryParameter returns the value of a query parameter as an integer.
|
||||
// If optional is set to true, will not return an error when the query parameter is not found.
|
||||
func RetrieveNumericQueryParameter(request *http.Request, name string, optional bool) (int, error) {
|
||||
queryParameter, err := RetrieveQueryParameter(request, name, optional)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if queryParameter == "" && optional {
|
||||
return 0, nil
|
||||
}
|
||||
return strconv.Atoi(queryParameter)
|
||||
}
|
||||
|
||||
// RetrieveBooleanQueryParameter returns the value of a query parameter as a boolean.
|
||||
// If optional is set to true, will not return an error when the query parameter is not found.
|
||||
func RetrieveBooleanQueryParameter(request *http.Request, name string, optional bool) (bool, error) {
|
||||
queryParameter, err := RetrieveQueryParameter(request, name, optional)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return queryParameter == "true", nil
|
||||
}
|
||||
|
||||
// RetrieveJSONQueryParameter decodes the value of a query parameter as a JSON object into the target parameter.
|
||||
// If optional is set to true, will not return an error when the query parameter is not found.
|
||||
func RetrieveJSONQueryParameter(request *http.Request, name string, target interface{}, optional bool) error {
|
||||
queryParameter, err := RetrieveQueryParameter(request, name, optional)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if queryParameter == "" {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal([]byte(queryParameter), target)
|
||||
}
|
45
pkg/libhttp/response/response.go
Normal file
45
pkg/libhttp/response/response.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Package response provides convenience functions to write into a http.ResponseWriter.
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
)
|
||||
|
||||
// JSON encodes data to rw in JSON format. Returns a pointer to a
|
||||
// HandlerError if encoding fails.
|
||||
func JSON(rw http.ResponseWriter, data interface{}) *httperror.HandlerError {
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err := json.NewEncoder(rw).Encode(data)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to write JSON response", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSON encodes data to rw in YAML format. Returns a pointer to a
|
||||
// HandlerError if encoding fails.
|
||||
func YAML(rw http.ResponseWriter, data interface{}) *httperror.HandlerError {
|
||||
rw.Header().Set("Content-Type", "text/yaml")
|
||||
|
||||
strData, ok := data.(string)
|
||||
if !ok {
|
||||
return httperror.InternalServerError("Unable to write YAML response", errors.New("failed to convert input to string"))
|
||||
}
|
||||
|
||||
fmt.Fprint(rw, strData)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Empty merely sets the response code to NoContent (204).
|
||||
func Empty(rw http.ResponseWriter) *httperror.HandlerError {
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue