1
0
Fork 0
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:
andres-portainer 2023-09-01 19:27:02 -03:00 committed by GitHub
parent 090fa4aeb3
commit 8cc5e0796c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
249 changed files with 1059 additions and 639 deletions

17
pkg/libhttp/LICENSE Normal file
View 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
View file

@ -0,0 +1,5 @@
[![GoDoc](https://godoc.org/github.com/portainer/libhttp?status.svg)](https://godoc.org/github.com/portainer/libhttp)
# libhttp
A HTTP library providing useful methods when working with `net/http` and `gorilla/mux`.

View 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})
}

View 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)
}

View 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
}

View 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)
}

View 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)
}

View 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
}