Merged in feature/openapifixes (pull request #106)
OpenAPI Middleware & Fixes * updates
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
type AuthenticationInput struct {
|
||||
RequestValidationInput *RequestValidationInput
|
||||
SecuritySchemeName string
|
||||
SecurityScheme *openapi3.SecurityScheme
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
func (input *AuthenticationInput) NewError(err error) error {
|
||||
if err == nil {
|
||||
if len(input.Scopes) == 0 {
|
||||
err = fmt.Errorf("security requirement %q failed", input.SecuritySchemeName)
|
||||
} else {
|
||||
err = fmt.Errorf("security requirement %q (scopes: %+v) failed", input.SecuritySchemeName, input.Scopes)
|
||||
}
|
||||
}
|
||||
return &RequestError{
|
||||
Input: input.RequestValidationInput,
|
||||
Reason: "authorization failed",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
var _ error = &RequestError{}
|
||||
|
||||
// RequestError is returned by ValidateRequest when request does not match OpenAPI spec
|
||||
type RequestError struct {
|
||||
Input *RequestValidationInput
|
||||
Parameter *openapi3.Parameter
|
||||
RequestBody *openapi3.RequestBody
|
||||
Reason string
|
||||
Err error
|
||||
}
|
||||
|
||||
var _ interface{ Unwrap() error } = RequestError{}
|
||||
|
||||
func (err *RequestError) Error() string {
|
||||
reason := err.Reason
|
||||
if e := err.Err; e != nil {
|
||||
if len(reason) == 0 || reason == e.Error() {
|
||||
reason = e.Error()
|
||||
} else {
|
||||
reason += ": " + e.Error()
|
||||
}
|
||||
}
|
||||
if v := err.Parameter; v != nil {
|
||||
return fmt.Sprintf("parameter %q in %s has an error: %s", v.Name, v.In, reason)
|
||||
} else if v := err.RequestBody; v != nil {
|
||||
return fmt.Sprintf("request body has an error: %s", reason)
|
||||
} else {
|
||||
return reason
|
||||
}
|
||||
}
|
||||
|
||||
func (err RequestError) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
var _ error = &ResponseError{}
|
||||
|
||||
// ResponseError is returned by ValidateResponse when response does not match OpenAPI spec
|
||||
type ResponseError struct {
|
||||
Input *ResponseValidationInput
|
||||
Reason string
|
||||
Err error
|
||||
}
|
||||
|
||||
var _ interface{ Unwrap() error } = ResponseError{}
|
||||
|
||||
func (err *ResponseError) Error() string {
|
||||
reason := err.Reason
|
||||
if e := err.Err; e != nil {
|
||||
if len(reason) == 0 {
|
||||
reason = e.Error()
|
||||
} else {
|
||||
reason += ": " + e.Error()
|
||||
}
|
||||
}
|
||||
return reason
|
||||
}
|
||||
|
||||
func (err ResponseError) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
var _ error = &SecurityRequirementsError{}
|
||||
|
||||
// SecurityRequirementsError is returned by ValidateSecurityRequirements
|
||||
// when no requirement is met.
|
||||
type SecurityRequirementsError struct {
|
||||
SecurityRequirements openapi3.SecurityRequirements
|
||||
Errors []error
|
||||
}
|
||||
|
||||
func (err *SecurityRequirementsError) Error() string {
|
||||
buff := bytes.NewBufferString("security requirements failed: ")
|
||||
for i, e := range err.Errors {
|
||||
buff.WriteString(e.Error())
|
||||
if i != len(err.Errors)-1 {
|
||||
buff.WriteString(" | ")
|
||||
}
|
||||
}
|
||||
|
||||
return buff.String()
|
||||
}
|
||||
|
||||
var _ interface{ Unwrap() []error } = SecurityRequirementsError{}
|
||||
|
||||
func (err SecurityRequirementsError) Unwrap() []error {
|
||||
return err.Errors
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseMediaType(contentType string) string {
|
||||
i := strings.IndexByte(contentType, ';')
|
||||
if i < 0 {
|
||||
return contentType
|
||||
}
|
||||
return contentType[:i]
|
||||
}
|
||||
|
||||
func isNilValue(value any) bool {
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
switch reflect.TypeOf(value).Kind() {
|
||||
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
|
||||
return reflect.ValueOf(value).IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
)
|
||||
|
||||
// Validator provides HTTP request and response validation middleware.
|
||||
type Validator struct {
|
||||
router routers.Router
|
||||
errFunc ErrFunc
|
||||
logFunc LogFunc
|
||||
strict bool
|
||||
options Options
|
||||
}
|
||||
|
||||
// ErrFunc handles errors that may occur during validation.
|
||||
type ErrFunc func(ctx context.Context, w http.ResponseWriter, status int, code ErrCode, err error)
|
||||
|
||||
// LogFunc handles log messages that may occur during validation.
|
||||
type LogFunc func(ctx context.Context, message string, err error)
|
||||
|
||||
// ErrCode is used for classification of different types of errors that may
|
||||
// occur during validation. These may be used to write an appropriate response
|
||||
// in ErrFunc.
|
||||
type ErrCode int
|
||||
|
||||
const (
|
||||
// ErrCodeOK indicates no error. It is also the default value.
|
||||
ErrCodeOK = 0
|
||||
// ErrCodeCannotFindRoute happens when the validator fails to resolve the
|
||||
// request to a defined OpenAPI route.
|
||||
ErrCodeCannotFindRoute = iota
|
||||
// ErrCodeRequestInvalid happens when the inbound request does not conform
|
||||
// to the OpenAPI 3 specification.
|
||||
ErrCodeRequestInvalid = iota
|
||||
// ErrCodeResponseInvalid happens when the wrapped handler response does
|
||||
// not conform to the OpenAPI 3 specification.
|
||||
ErrCodeResponseInvalid = iota
|
||||
)
|
||||
|
||||
func (e ErrCode) responseText() string {
|
||||
switch e {
|
||||
case ErrCodeOK:
|
||||
return "OK"
|
||||
case ErrCodeCannotFindRoute:
|
||||
return "not found"
|
||||
case ErrCodeRequestInvalid:
|
||||
return "bad request"
|
||||
default:
|
||||
return "server error"
|
||||
}
|
||||
}
|
||||
|
||||
// NewValidator returns a new response validation middleware, using the given
|
||||
// routes from an OpenAPI 3 specification.
|
||||
func NewValidator(router routers.Router, options ...ValidatorOption) *Validator {
|
||||
v := &Validator{
|
||||
router: router,
|
||||
errFunc: func(_ context.Context, w http.ResponseWriter, status int, code ErrCode, _ error) {
|
||||
http.Error(w, code.responseText(), status)
|
||||
},
|
||||
logFunc: func(_ context.Context, message string, err error) {
|
||||
log.Printf("%s: %v", message, err)
|
||||
},
|
||||
}
|
||||
for i := range options {
|
||||
options[i](v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ValidatorOption defines an option that may be specified when creating a
|
||||
// Validator.
|
||||
type ValidatorOption func(*Validator)
|
||||
|
||||
// OnErr provides a callback that handles writing an HTTP response on a
|
||||
// validation error. This allows customization of error responses without
|
||||
// prescribing a particular form. This callback is only called on response
|
||||
// validator errors in Strict mode.
|
||||
func OnErr(f ErrFunc) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.errFunc = f
|
||||
}
|
||||
}
|
||||
|
||||
// OnLog provides a callback that handles logging in the Validator. This allows
|
||||
// the validator to integrate with a services' existing logging system without
|
||||
// prescribing a particular one.
|
||||
func OnLog(f LogFunc) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.logFunc = f
|
||||
}
|
||||
}
|
||||
|
||||
// Strict, if set, causes an internal server error to be sent if the wrapped
|
||||
// handler response fails response validation. If not set, the response is sent
|
||||
// and the error is only logged.
|
||||
func Strict(strict bool) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.strict = strict
|
||||
}
|
||||
}
|
||||
|
||||
// ValidationOptions sets request/response validation options on the validator.
|
||||
func ValidationOptions(options Options) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.options = options
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware returns an http.Handler which wraps the given handler with
|
||||
// request and response validation.
|
||||
func (v *Validator) Middleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
route, pathParams, err := v.router.FindRoute(r)
|
||||
if err != nil {
|
||||
v.logFunc(ctx, "validation error: failed to find route for "+r.URL.String(), err)
|
||||
v.errFunc(ctx, w, http.StatusNotFound, ErrCodeCannotFindRoute, err)
|
||||
return
|
||||
}
|
||||
requestValidationInput := &RequestValidationInput{
|
||||
Request: r,
|
||||
PathParams: pathParams,
|
||||
Route: route,
|
||||
Options: &v.options,
|
||||
}
|
||||
if err = ValidateRequest(ctx, requestValidationInput); err != nil {
|
||||
v.logFunc(ctx, "invalid request", err)
|
||||
v.errFunc(ctx, w, http.StatusBadRequest, ErrCodeRequestInvalid, err)
|
||||
return
|
||||
}
|
||||
|
||||
var wr responseWrapper
|
||||
if v.strict {
|
||||
wr = &strictResponseWrapper{w: w}
|
||||
} else {
|
||||
wr = newWarnResponseWrapper(w)
|
||||
}
|
||||
|
||||
h.ServeHTTP(wr, r)
|
||||
|
||||
if err = ValidateResponse(ctx, &ResponseValidationInput{
|
||||
RequestValidationInput: requestValidationInput,
|
||||
Status: wr.statusCode(),
|
||||
Header: wr.Header(),
|
||||
Body: io.NopCloser(bytes.NewBuffer(wr.bodyContents())),
|
||||
Options: &v.options,
|
||||
}); err != nil {
|
||||
v.logFunc(ctx, "invalid response", err)
|
||||
if v.strict {
|
||||
v.errFunc(ctx, w, http.StatusInternalServerError, ErrCodeResponseInvalid, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = wr.flushBodyContents(); err != nil {
|
||||
v.logFunc(ctx, "failed to write response", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type responseWrapper interface {
|
||||
http.ResponseWriter
|
||||
|
||||
// flushBodyContents writes the buffered response to the client, if it has
|
||||
// not yet been written.
|
||||
flushBodyContents() error
|
||||
|
||||
// statusCode returns the response status code, 0 if not set yet.
|
||||
statusCode() int
|
||||
|
||||
// bodyContents returns the buffered
|
||||
bodyContents() []byte
|
||||
}
|
||||
|
||||
type warnResponseWrapper struct {
|
||||
w http.ResponseWriter
|
||||
headerWritten bool
|
||||
status int
|
||||
body bytes.Buffer
|
||||
tee io.Writer
|
||||
}
|
||||
|
||||
func newWarnResponseWrapper(w http.ResponseWriter) *warnResponseWrapper {
|
||||
wr := &warnResponseWrapper{
|
||||
w: w,
|
||||
}
|
||||
wr.tee = io.MultiWriter(w, &wr.body)
|
||||
return wr
|
||||
}
|
||||
|
||||
// Write implements http.ResponseWriter.
|
||||
func (wr *warnResponseWrapper) Write(b []byte) (int, error) {
|
||||
if !wr.headerWritten {
|
||||
wr.WriteHeader(http.StatusOK)
|
||||
}
|
||||
return wr.tee.Write(b)
|
||||
}
|
||||
|
||||
// WriteHeader implements http.ResponseWriter.
|
||||
func (wr *warnResponseWrapper) WriteHeader(status int) {
|
||||
if !wr.headerWritten {
|
||||
// If the header hasn't been written, record the status for response
|
||||
// validation.
|
||||
wr.status = status
|
||||
wr.headerWritten = true
|
||||
}
|
||||
wr.w.WriteHeader(wr.status)
|
||||
}
|
||||
|
||||
// Header implements http.ResponseWriter.
|
||||
func (wr *warnResponseWrapper) Header() http.Header {
|
||||
return wr.w.Header()
|
||||
}
|
||||
|
||||
// Flush implements the optional http.Flusher interface.
|
||||
func (wr *warnResponseWrapper) Flush() {
|
||||
// If the wrapped http.ResponseWriter implements optional http.Flusher,
|
||||
// pass through.
|
||||
if fl, ok := wr.w.(http.Flusher); ok {
|
||||
fl.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (wr *warnResponseWrapper) flushBodyContents() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wr *warnResponseWrapper) statusCode() int {
|
||||
return wr.status
|
||||
}
|
||||
|
||||
func (wr *warnResponseWrapper) bodyContents() []byte {
|
||||
return wr.body.Bytes()
|
||||
}
|
||||
|
||||
type strictResponseWrapper struct {
|
||||
w http.ResponseWriter
|
||||
headerWritten bool
|
||||
status int
|
||||
body bytes.Buffer
|
||||
}
|
||||
|
||||
// Write implements http.ResponseWriter.
|
||||
func (wr *strictResponseWrapper) Write(b []byte) (int, error) {
|
||||
if !wr.headerWritten {
|
||||
wr.WriteHeader(http.StatusOK)
|
||||
}
|
||||
return wr.body.Write(b)
|
||||
}
|
||||
|
||||
// WriteHeader implements http.ResponseWriter.
|
||||
func (wr *strictResponseWrapper) WriteHeader(status int) {
|
||||
if !wr.headerWritten {
|
||||
wr.status = status
|
||||
wr.headerWritten = true
|
||||
}
|
||||
}
|
||||
|
||||
// Header implements http.ResponseWriter.
|
||||
func (wr *strictResponseWrapper) Header() http.Header {
|
||||
return wr.w.Header()
|
||||
}
|
||||
|
||||
func (wr *strictResponseWrapper) flushBodyContents() error {
|
||||
wr.w.WriteHeader(wr.status)
|
||||
_, err := wr.w.Write(wr.body.Bytes())
|
||||
return err
|
||||
}
|
||||
|
||||
func (wr *strictResponseWrapper) statusCode() int {
|
||||
return wr.status
|
||||
}
|
||||
|
||||
func (wr *strictResponseWrapper) bodyContents() []byte {
|
||||
return wr.body.Bytes()
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package openapi3filter
|
||||
|
||||
import "github.com/getkin/kin-openapi/openapi3"
|
||||
|
||||
// Options used by ValidateRequest and ValidateResponse
|
||||
type Options struct {
|
||||
// Set ExcludeRequestBody so ValidateRequest skips request body validation
|
||||
ExcludeRequestBody bool
|
||||
|
||||
// Set ExcludeRequestQueryParams so ValidateRequest skips request query params validation
|
||||
ExcludeRequestQueryParams bool
|
||||
|
||||
// Set ExcludeResponseBody so ValidateResponse skips response body validation
|
||||
ExcludeResponseBody bool
|
||||
|
||||
// Set ExcludeReadOnlyValidations so ValidateRequest skips read-only validations
|
||||
ExcludeReadOnlyValidations bool
|
||||
|
||||
// Set ExcludeWriteOnlyValidations so ValidateResponse skips write-only validations
|
||||
ExcludeWriteOnlyValidations bool
|
||||
|
||||
// Set IncludeResponseStatus so ValidateResponse fails on response
|
||||
// status not defined in OpenAPI spec
|
||||
IncludeResponseStatus bool
|
||||
|
||||
MultiError bool
|
||||
|
||||
// Set RegexCompiler to override the regex implementation
|
||||
RegexCompiler openapi3.RegexCompilerFunc
|
||||
|
||||
// A document with security schemes defined will not pass validation
|
||||
// unless an AuthenticationFunc is defined.
|
||||
// See NoopAuthenticationFunc
|
||||
AuthenticationFunc AuthenticationFunc
|
||||
|
||||
// Indicates whether default values are set in the
|
||||
// request. If true, then they are not set
|
||||
SkipSettingDefaults bool
|
||||
|
||||
customSchemaErrorFunc CustomSchemaErrorFunc
|
||||
}
|
||||
|
||||
// CustomSchemaErrorFunc allows for custom the schema error message.
|
||||
type CustomSchemaErrorFunc func(err *openapi3.SchemaError) string
|
||||
|
||||
// WithCustomSchemaErrorFunc sets a function to override the schema error message.
|
||||
// If the passed function returns an empty string, it returns to the previous Error() implementation.
|
||||
func (o *Options) WithCustomSchemaErrorFunc(f CustomSchemaErrorFunc) {
|
||||
o.customSchemaErrorFunc = f
|
||||
}
|
||||
+1591
File diff suppressed because it is too large
Load Diff
+58
@@ -0,0 +1,58 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func encodeBody(body any, mediaType string) ([]byte, error) {
|
||||
if encoder := RegisteredBodyEncoder(mediaType); encoder != nil {
|
||||
return encoder(body)
|
||||
}
|
||||
return nil, &ParseError{
|
||||
Kind: KindUnsupportedFormat,
|
||||
Reason: fmt.Sprintf("%s %q", prefixUnsupportedCT, mediaType),
|
||||
}
|
||||
}
|
||||
|
||||
// BodyEncoder really is an (encoding/json).Marshaler
|
||||
type BodyEncoder func(body any) ([]byte, error)
|
||||
|
||||
var bodyEncodersM sync.RWMutex
|
||||
var bodyEncoders = map[string]BodyEncoder{
|
||||
"application/json": json.Marshal,
|
||||
}
|
||||
|
||||
// RegisterBodyEncoder enables package-wide decoding of contentType values
|
||||
func RegisterBodyEncoder(contentType string, encoder BodyEncoder) {
|
||||
if contentType == "" {
|
||||
panic("contentType is empty")
|
||||
}
|
||||
if encoder == nil {
|
||||
panic("encoder is not defined")
|
||||
}
|
||||
bodyEncodersM.Lock()
|
||||
bodyEncoders[contentType] = encoder
|
||||
bodyEncodersM.Unlock()
|
||||
}
|
||||
|
||||
// UnregisterBodyEncoder disables package-wide decoding of contentType values
|
||||
func UnregisterBodyEncoder(contentType string) {
|
||||
if contentType == "" {
|
||||
panic("contentType is empty")
|
||||
}
|
||||
bodyEncodersM.Lock()
|
||||
delete(bodyEncoders, contentType)
|
||||
bodyEncodersM.Unlock()
|
||||
}
|
||||
|
||||
// RegisteredBodyEncoder returns the registered body encoder for the given content type.
|
||||
//
|
||||
// If no encoder was registered for the given content type, nil is returned.
|
||||
func RegisteredBodyEncoder(contentType string) BodyEncoder {
|
||||
bodyEncodersM.RLock()
|
||||
mayBE := bodyEncoders[contentType]
|
||||
bodyEncodersM.RUnlock()
|
||||
return mayBE
|
||||
}
|
||||
+431
@@ -0,0 +1,431 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
// ErrAuthenticationServiceMissing is returned when no authentication service
|
||||
// is defined for the request validator
|
||||
var ErrAuthenticationServiceMissing = errors.New("missing AuthenticationFunc")
|
||||
|
||||
// ErrInvalidRequired is returned when a required value of a parameter or request body is not defined.
|
||||
var ErrInvalidRequired = errors.New("value is required but missing")
|
||||
|
||||
// ErrInvalidEmptyValue is returned when a value of a parameter or request body is empty while it's not allowed.
|
||||
var ErrInvalidEmptyValue = errors.New("empty value is not allowed")
|
||||
|
||||
// ValidateRequest is used to validate the given input according to previous
|
||||
// loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a
|
||||
// non-nil error will be returned.
|
||||
//
|
||||
// Note: One can tune the behavior of uniqueItems: true verification
|
||||
// by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker
|
||||
func ValidateRequest(ctx context.Context, input *RequestValidationInput) error {
|
||||
var me openapi3.MultiError
|
||||
|
||||
options := input.Options
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
route := input.Route
|
||||
operation := route.Operation
|
||||
operationParameters := operation.Parameters
|
||||
pathItemParameters := route.PathItem.Parameters
|
||||
|
||||
// Security
|
||||
security := operation.Security
|
||||
// If there aren't any security requirements for the operation
|
||||
if security == nil {
|
||||
// Use the global security requirements.
|
||||
security = &route.Spec.Security
|
||||
}
|
||||
if security != nil {
|
||||
if err := ValidateSecurityRequirements(ctx, input, *security); err != nil {
|
||||
if !options.MultiError {
|
||||
return err
|
||||
}
|
||||
me = append(me, err)
|
||||
}
|
||||
}
|
||||
|
||||
// For each parameter of the PathItem
|
||||
for _, parameterRef := range pathItemParameters {
|
||||
parameter := parameterRef.Value
|
||||
if operationParameters != nil {
|
||||
if override := operationParameters.GetByInAndName(parameter.In, parameter.Name); override != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := ValidateParameter(ctx, input, parameter); err != nil {
|
||||
if !options.MultiError {
|
||||
return err
|
||||
}
|
||||
me = append(me, err)
|
||||
}
|
||||
}
|
||||
|
||||
// For each parameter of the Operation
|
||||
for _, parameter := range operationParameters {
|
||||
if options.ExcludeRequestQueryParams && parameter.Value.In == openapi3.ParameterInQuery {
|
||||
continue
|
||||
}
|
||||
if err := ValidateParameter(ctx, input, parameter.Value); err != nil {
|
||||
if !options.MultiError {
|
||||
return err
|
||||
}
|
||||
me = append(me, err)
|
||||
}
|
||||
}
|
||||
|
||||
// RequestBody
|
||||
requestBody := operation.RequestBody
|
||||
if requestBody != nil && !options.ExcludeRequestBody {
|
||||
if err := ValidateRequestBody(ctx, input, requestBody.Value); err != nil {
|
||||
if !options.MultiError {
|
||||
return err
|
||||
}
|
||||
me = append(me, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(me) > 0 {
|
||||
return me
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// appendToQueryValues adds to query parameters each value in the provided slice
|
||||
func appendToQueryValues[T any](q url.Values, parameterName string, v []T) {
|
||||
for _, i := range v {
|
||||
q.Add(parameterName, fmt.Sprintf("%v", i))
|
||||
}
|
||||
}
|
||||
|
||||
// populateDefaultQueryParameters populates default values inside query parameters, while ensuring types are respected
|
||||
func populateDefaultQueryParameters(q url.Values, parameterName string, value any) {
|
||||
switch t := value.(type) {
|
||||
case []any:
|
||||
appendToQueryValues(q, parameterName, t)
|
||||
default:
|
||||
q.Add(parameterName, fmt.Sprintf("%v", value))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ValidateParameter validates a parameter's value by JSON schema.
|
||||
// The function returns RequestError with a ParseError cause when unable to parse a value.
|
||||
// The function returns RequestError with ErrInvalidRequired cause when a value of a required parameter is not defined.
|
||||
// The function returns RequestError with ErrInvalidEmptyValue cause when a value of a required parameter is not defined.
|
||||
// The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema.
|
||||
func ValidateParameter(ctx context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error {
|
||||
if parameter.Schema == nil && parameter.Content == nil {
|
||||
// We have no schema for the parameter. Assume that everything passes
|
||||
// a schema-less check, but this could also be an error. The OpenAPI
|
||||
// validation allows this to happen.
|
||||
return nil
|
||||
}
|
||||
|
||||
options := input.Options
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
|
||||
var value any
|
||||
var err error
|
||||
var found bool
|
||||
var schema *openapi3.Schema
|
||||
|
||||
// Validation will ensure that we either have content or schema.
|
||||
if parameter.Content != nil {
|
||||
if value, schema, found, err = decodeContentParameter(parameter, input); err != nil {
|
||||
return &RequestError{Input: input, Parameter: parameter, Err: err}
|
||||
}
|
||||
} else {
|
||||
if value, found, err = decodeStyledParameter(parameter, input); err != nil {
|
||||
return &RequestError{Input: input, Parameter: parameter, Err: err}
|
||||
}
|
||||
schema = parameter.Schema.Value
|
||||
}
|
||||
|
||||
// Set default value if needed
|
||||
if !options.SkipSettingDefaults && value == nil && schema != nil {
|
||||
value = schema.Default
|
||||
for _, subSchema := range schema.AllOf {
|
||||
if subSchema.Value.Default != nil {
|
||||
value = subSchema.Value.Default
|
||||
break // This is not a validation of the schema itself, so use the first default value.
|
||||
}
|
||||
}
|
||||
|
||||
if value != nil {
|
||||
req := input.Request
|
||||
switch parameter.In {
|
||||
case openapi3.ParameterInPath:
|
||||
// Path parameters are required.
|
||||
// Next check `parameter.Required && !found` will catch this.
|
||||
case openapi3.ParameterInQuery:
|
||||
q := req.URL.Query()
|
||||
populateDefaultQueryParameters(q, parameter.Name, value)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
case openapi3.ParameterInHeader:
|
||||
req.Header.Add(parameter.Name, fmt.Sprintf("%v", value))
|
||||
case openapi3.ParameterInCookie:
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: parameter.Name,
|
||||
Value: fmt.Sprintf("%v", value),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate a parameter's value and presence.
|
||||
if parameter.Required && !found {
|
||||
return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidRequired.Error(), Err: ErrInvalidRequired}
|
||||
}
|
||||
|
||||
if isNilValue(value) {
|
||||
if !parameter.AllowEmptyValue && found {
|
||||
return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidEmptyValue.Error(), Err: ErrInvalidEmptyValue}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if schema == nil {
|
||||
// A parameter's schema is not defined so skip validation of a parameter's value.
|
||||
return nil
|
||||
}
|
||||
|
||||
var opts []openapi3.SchemaValidationOption
|
||||
if options.MultiError {
|
||||
opts = make([]openapi3.SchemaValidationOption, 0, 1)
|
||||
opts = append(opts, openapi3.MultiErrors())
|
||||
}
|
||||
if options.customSchemaErrorFunc != nil {
|
||||
opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
|
||||
}
|
||||
if err = schema.VisitJSON(value, opts...); err != nil {
|
||||
return &RequestError{Input: input, Parameter: parameter, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const prefixInvalidCT = "header Content-Type has unexpected value"
|
||||
|
||||
// ValidateRequestBody validates data of a request's body.
|
||||
//
|
||||
// The function returns RequestError with ErrInvalidRequired cause when a value is required but not defined.
|
||||
// The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema.
|
||||
func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, requestBody *openapi3.RequestBody) error {
|
||||
var (
|
||||
req = input.Request
|
||||
data []byte
|
||||
)
|
||||
|
||||
options := input.Options
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
|
||||
if req.Body != http.NoBody && req.Body != nil {
|
||||
defer req.Body.Close()
|
||||
var err error
|
||||
if data, err = io.ReadAll(req.Body); err != nil {
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
RequestBody: requestBody,
|
||||
Reason: "reading failed",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
// Put the data back into the input
|
||||
req.Body = nil
|
||||
if req.GetBody != nil {
|
||||
if req.Body, err = req.GetBody(); err != nil {
|
||||
req.Body = nil
|
||||
}
|
||||
}
|
||||
if req.Body == nil {
|
||||
req.ContentLength = int64(len(data))
|
||||
req.GetBody = func() (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewReader(data)), nil
|
||||
}
|
||||
req.Body, _ = req.GetBody() // no error return
|
||||
}
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
if requestBody.Required {
|
||||
return &RequestError{Input: input, RequestBody: requestBody, Err: ErrInvalidRequired}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
content := requestBody.Content
|
||||
if len(content) == 0 {
|
||||
// A request's body does not have declared content, so skip validation.
|
||||
return nil
|
||||
}
|
||||
|
||||
inputMIME := req.Header.Get(headerCT)
|
||||
contentType := requestBody.Content.Get(inputMIME)
|
||||
if contentType == nil {
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
RequestBody: requestBody,
|
||||
Reason: fmt.Sprintf("%s %q", prefixInvalidCT, inputMIME),
|
||||
}
|
||||
}
|
||||
|
||||
if contentType.Schema == nil {
|
||||
// A JSON schema that describes the received data is not declared, so skip validation.
|
||||
return nil
|
||||
}
|
||||
|
||||
encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] }
|
||||
mediaType, value, err := decodeBody(bytes.NewReader(data), req.Header, contentType.Schema, encFn)
|
||||
if err != nil {
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
RequestBody: requestBody,
|
||||
Reason: "failed to decode request body",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
defaultsSet := false
|
||||
opts := make([]openapi3.SchemaValidationOption, 0, 4) // 4 potential opts here
|
||||
opts = append(opts, openapi3.VisitAsRequest())
|
||||
if !options.SkipSettingDefaults {
|
||||
opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true }))
|
||||
}
|
||||
if options.MultiError {
|
||||
opts = append(opts, openapi3.MultiErrors())
|
||||
}
|
||||
if options.customSchemaErrorFunc != nil {
|
||||
opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
|
||||
}
|
||||
if options.ExcludeReadOnlyValidations {
|
||||
opts = append(opts, openapi3.DisableReadOnlyValidation())
|
||||
}
|
||||
if options.RegexCompiler != nil {
|
||||
opts = append(opts, openapi3.SetSchemaRegexCompiler(options.RegexCompiler))
|
||||
}
|
||||
|
||||
// Validate JSON with the schema
|
||||
if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil {
|
||||
schemaId := getSchemaIdentifier(contentType.Schema)
|
||||
schemaId = prependSpaceIfNeeded(schemaId)
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
RequestBody: requestBody,
|
||||
Reason: fmt.Sprintf("doesn't match schema%s", schemaId),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if defaultsSet {
|
||||
var err error
|
||||
if data, err = encodeBody(value, mediaType); err != nil {
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
RequestBody: requestBody,
|
||||
Reason: "rewriting failed",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
// Put the data back into the input
|
||||
if req.Body != nil {
|
||||
req.Body.Close()
|
||||
}
|
||||
req.ContentLength = int64(len(data))
|
||||
req.GetBody = func() (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewReader(data)), nil
|
||||
}
|
||||
req.Body, _ = req.GetBody() // no error return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSecurityRequirements goes through multiple OpenAPI 3 security
|
||||
// requirements in order and returns nil on the first valid requirement.
|
||||
// If no requirement is met, errors are returned in order.
|
||||
func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationInput, srs openapi3.SecurityRequirements) error {
|
||||
if len(srs) == 0 {
|
||||
return nil
|
||||
}
|
||||
var errs []error
|
||||
for _, sr := range srs {
|
||||
if err := validateSecurityRequirement(ctx, input, sr); err != nil {
|
||||
if len(errs) == 0 {
|
||||
errs = make([]error, 0, len(srs))
|
||||
}
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return &SecurityRequirementsError{
|
||||
SecurityRequirements: srs,
|
||||
Errors: errs,
|
||||
}
|
||||
}
|
||||
|
||||
// validateSecurityRequirement validates a single OpenAPI 3 security requirement
|
||||
func validateSecurityRequirement(ctx context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error {
|
||||
names := make([]string, 0, len(securityRequirement))
|
||||
for name := range securityRequirement {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
// Get authentication function
|
||||
options := input.Options
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
f := options.AuthenticationFunc
|
||||
if f == nil {
|
||||
return ErrAuthenticationServiceMissing
|
||||
}
|
||||
|
||||
var securitySchemes openapi3.SecuritySchemes
|
||||
if components := input.Route.Spec.Components; components != nil {
|
||||
securitySchemes = components.SecuritySchemes
|
||||
}
|
||||
|
||||
// For each scheme for the requirement
|
||||
for _, name := range names {
|
||||
var securityScheme *openapi3.SecurityScheme
|
||||
if securitySchemes != nil {
|
||||
if ref := securitySchemes[name]; ref != nil {
|
||||
securityScheme = ref.Value
|
||||
}
|
||||
}
|
||||
if securityScheme == nil {
|
||||
return &RequestError{
|
||||
Input: input,
|
||||
Err: fmt.Errorf("security scheme %q is not declared", name),
|
||||
}
|
||||
}
|
||||
scopes := securityRequirement[name]
|
||||
if err := f(ctx, &AuthenticationInput{
|
||||
RequestValidationInput: input,
|
||||
SecuritySchemeName: name,
|
||||
SecurityScheme: securityScheme,
|
||||
Scopes: scopes,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
)
|
||||
|
||||
// A ContentParameterDecoder takes a parameter definition from the OpenAPI spec,
|
||||
// and the value which we received for it. It is expected to return the
|
||||
// value unmarshaled into an interface which can be traversed for
|
||||
// validation, it should also return the schema to be used for validating the
|
||||
// object, since there can be more than one in the content spec.
|
||||
//
|
||||
// If a query parameter appears multiple times, values[] will have more
|
||||
// than one value, but for all other parameter types it should have just
|
||||
// one.
|
||||
type ContentParameterDecoder func(param *openapi3.Parameter, values []string) (any, *openapi3.Schema, error)
|
||||
|
||||
type RequestValidationInput struct {
|
||||
Request *http.Request
|
||||
PathParams map[string]string
|
||||
QueryParams url.Values
|
||||
Route *routers.Route
|
||||
Options *Options
|
||||
ParamDecoder ContentParameterDecoder
|
||||
}
|
||||
|
||||
func (input *RequestValidationInput) GetQueryParams() url.Values {
|
||||
q := input.QueryParams
|
||||
if q == nil {
|
||||
q = input.Request.URL.Query()
|
||||
input.QueryParams = q
|
||||
}
|
||||
return q
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
// Package openapi3filter validates that requests and inputs request an OpenAPI 3 specification file.
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
// ValidateResponse is used to validate the given input according to previous
|
||||
// loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a
|
||||
// non-nil error will be returned.
|
||||
//
|
||||
// Note: One can tune the behavior of uniqueItems: true verification
|
||||
// by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker
|
||||
func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error {
|
||||
req := input.RequestValidationInput.Request
|
||||
switch req.Method {
|
||||
case "HEAD":
|
||||
return nil
|
||||
}
|
||||
status := input.Status
|
||||
|
||||
// These status codes will never be validated.
|
||||
// TODO: The list is probably missing some.
|
||||
switch status {
|
||||
case http.StatusNotModified,
|
||||
http.StatusPermanentRedirect,
|
||||
http.StatusTemporaryRedirect,
|
||||
http.StatusMovedPermanently:
|
||||
return nil
|
||||
}
|
||||
route := input.RequestValidationInput.Route
|
||||
options := input.Options
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
|
||||
// Find input for the current status
|
||||
responses := route.Operation.Responses
|
||||
if responses.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
responseRef := responses.Status(status) // Response
|
||||
if responseRef == nil {
|
||||
responseRef = responses.Default() // Default input
|
||||
}
|
||||
if responseRef == nil {
|
||||
// By default, status that is not documented is allowed.
|
||||
if !options.IncludeResponseStatus {
|
||||
return nil
|
||||
}
|
||||
return &ResponseError{Input: input, Reason: "status is not supported"}
|
||||
}
|
||||
response := responseRef.Value
|
||||
if response == nil {
|
||||
return &ResponseError{Input: input, Reason: "response has not been resolved"}
|
||||
}
|
||||
|
||||
opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential options here
|
||||
if options.MultiError {
|
||||
opts = append(opts, openapi3.MultiErrors())
|
||||
}
|
||||
if options.customSchemaErrorFunc != nil {
|
||||
opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
|
||||
}
|
||||
if options.ExcludeWriteOnlyValidations {
|
||||
opts = append(opts, openapi3.DisableWriteOnlyValidation())
|
||||
}
|
||||
|
||||
headers := make([]string, 0, len(response.Headers))
|
||||
for k := range response.Headers {
|
||||
if k != headerCT {
|
||||
headers = append(headers, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(headers)
|
||||
for _, headerName := range headers {
|
||||
headerRef := response.Headers[headerName]
|
||||
if err := validateResponseHeader(headerName, headerRef, input, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if options.ExcludeResponseBody {
|
||||
// A user turned off validation of a response's body.
|
||||
return nil
|
||||
}
|
||||
|
||||
content := response.Content
|
||||
if len(content) == 0 || options.ExcludeResponseBody {
|
||||
// An operation does not contains a validation schema for responses with this status code.
|
||||
return nil
|
||||
}
|
||||
|
||||
inputMIME := input.Header.Get(headerCT)
|
||||
contentType := content.Get(inputMIME)
|
||||
if contentType == nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("response %s: %q", prefixInvalidCT, inputMIME),
|
||||
}
|
||||
}
|
||||
|
||||
if contentType.Schema == nil {
|
||||
// An operation does not contains a validation schema for responses with this status code.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read response's body.
|
||||
body := input.Body
|
||||
|
||||
// Response would contain partial or empty input body
|
||||
// after we begin reading.
|
||||
// Ensure that this doesn't happen.
|
||||
input.Body = nil
|
||||
|
||||
// Ensure we close the reader
|
||||
defer body.Close()
|
||||
|
||||
// Read all
|
||||
data, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: "failed to read response body",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Put the data back into the response.
|
||||
input.SetBodyBytes(data)
|
||||
|
||||
encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] }
|
||||
_, value, err := decodeBody(bytes.NewBuffer(data), input.Header, contentType.Schema, encFn)
|
||||
if err != nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: "failed to decode response body",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate data with the schema.
|
||||
if err := contentType.Schema.Value.VisitJSON(value, append(opts, openapi3.VisitAsResponse())...); err != nil {
|
||||
schemaId := getSchemaIdentifier(contentType.Schema)
|
||||
schemaId = prependSpaceIfNeeded(schemaId)
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("response body doesn't match schema%s", schemaId),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateResponseHeader(headerName string, headerRef *openapi3.HeaderRef, input *ResponseValidationInput, opts []openapi3.SchemaValidationOption) error {
|
||||
var err error
|
||||
var decodedValue any
|
||||
var found bool
|
||||
var sm *openapi3.SerializationMethod
|
||||
dec := &headerParamDecoder{header: input.Header}
|
||||
|
||||
if sm, err = headerRef.Value.SerializationMethod(); err != nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("unable to get header %q serialization method", headerName),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if decodedValue, found, err = decodeValue(dec, headerName, sm, headerRef.Value.Schema, headerRef.Value.Required); err != nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("unable to decode header %q value", headerName),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
if err = headerRef.Value.Schema.Value.VisitJSON(decodedValue, opts...); err != nil {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("response header %q doesn't match schema", headerName),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
} else if headerRef.Value.Required {
|
||||
return &ResponseError{
|
||||
Input: input,
|
||||
Reason: fmt.Sprintf("response header %q missing", headerName),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSchemaIdentifier gets something by which a schema could be identified.
|
||||
// A schema by itself doesn't have a true identity field. This function makes
|
||||
// a best effort to get a value that can fill that void.
|
||||
func getSchemaIdentifier(schema *openapi3.SchemaRef) string {
|
||||
var id string
|
||||
|
||||
if schema != nil {
|
||||
id = strings.TrimSpace(schema.Ref)
|
||||
}
|
||||
if id == "" && schema.Value != nil {
|
||||
id = strings.TrimSpace(schema.Value.Title)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func prependSpaceIfNeeded(value string) string {
|
||||
if len(value) > 0 {
|
||||
value = " " + value
|
||||
}
|
||||
return value
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ResponseValidationInput struct {
|
||||
RequestValidationInput *RequestValidationInput
|
||||
Status int
|
||||
Header http.Header
|
||||
Body io.ReadCloser
|
||||
Options *Options
|
||||
}
|
||||
|
||||
func (input *ResponseValidationInput) SetBodyBytes(value []byte) *ResponseValidationInput {
|
||||
input.Body = io.NopCloser(bytes.NewReader(value))
|
||||
return input
|
||||
}
|
||||
|
||||
var JSONPrefixes = []string{
|
||||
")]}',\n",
|
||||
}
|
||||
|
||||
// TrimJSONPrefix trims one of the possible prefixes
|
||||
func TrimJSONPrefix(data []byte) []byte {
|
||||
search:
|
||||
for _, prefix := range JSONPrefixes {
|
||||
if len(data) < len(prefix) {
|
||||
continue
|
||||
}
|
||||
for i, b := range data[:len(prefix)] {
|
||||
if b != prefix[i] {
|
||||
continue search
|
||||
}
|
||||
}
|
||||
return data[len(prefix):]
|
||||
}
|
||||
return data
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ValidationError struct provides granular error information
|
||||
// useful for communicating issues back to end user and developer.
|
||||
// Based on https://jsonapi.org/format/#error-objects
|
||||
type ValidationError struct {
|
||||
// A unique identifier for this particular occurrence of the problem.
|
||||
Id string `json:"id,omitempty" yaml:"id,omitempty"`
|
||||
// The HTTP status code applicable to this problem.
|
||||
Status int `json:"status,omitempty" yaml:"status,omitempty"`
|
||||
// An application-specific error code, expressed as a string value.
|
||||
Code string `json:"code,omitempty" yaml:"code,omitempty"`
|
||||
// A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.
|
||||
Title string `json:"title,omitempty" yaml:"title,omitempty"`
|
||||
// A human-readable explanation specific to this occurrence of the problem.
|
||||
Detail string `json:"detail,omitempty" yaml:"detail,omitempty"`
|
||||
// An object containing references to the source of the error
|
||||
Source *ValidationErrorSource `json:"source,omitempty" yaml:"source,omitempty"`
|
||||
}
|
||||
|
||||
// ValidationErrorSource struct
|
||||
type ValidationErrorSource struct {
|
||||
// A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].
|
||||
Pointer string `json:"pointer,omitempty" yaml:"pointer,omitempty"`
|
||||
// A string indicating which query parameter caused the error.
|
||||
Parameter string `json:"parameter,omitempty" yaml:"parameter,omitempty"`
|
||||
}
|
||||
|
||||
var _ error = &ValidationError{}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *ValidationError) Error() string {
|
||||
b := bytes.NewBufferString("[")
|
||||
if e.Status != 0 {
|
||||
b.WriteString(strconv.Itoa(e.Status))
|
||||
}
|
||||
b.WriteString("]")
|
||||
b.WriteString("[")
|
||||
if e.Code != "" {
|
||||
b.WriteString(e.Code)
|
||||
}
|
||||
b.WriteString("]")
|
||||
b.WriteString("[")
|
||||
if e.Id != "" {
|
||||
b.WriteString(e.Id)
|
||||
}
|
||||
b.WriteString("]")
|
||||
b.WriteString(" ")
|
||||
if e.Title != "" {
|
||||
b.WriteString(e.Title)
|
||||
b.WriteString(" ")
|
||||
}
|
||||
if e.Detail != "" {
|
||||
b.WriteString("| ")
|
||||
b.WriteString(e.Detail)
|
||||
b.WriteString(" ")
|
||||
}
|
||||
if e.Source != nil {
|
||||
b.WriteString("[source ")
|
||||
if e.Source.Parameter != "" {
|
||||
b.WriteString("parameter=")
|
||||
b.WriteString(e.Source.Parameter)
|
||||
} else if e.Source.Pointer != "" {
|
||||
b.WriteString("pointer=")
|
||||
b.WriteString(e.Source.Pointer)
|
||||
}
|
||||
b.WriteString("]")
|
||||
}
|
||||
|
||||
if b.Len() == 0 {
|
||||
return "no error"
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// StatusCode implements the StatusCoder interface for DefaultErrorEncoder
|
||||
func (e *ValidationError) StatusCode() int {
|
||||
return e.Status
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
)
|
||||
|
||||
// ValidationErrorEncoder wraps a base ErrorEncoder to handle ValidationErrors
|
||||
type ValidationErrorEncoder struct {
|
||||
Encoder ErrorEncoder
|
||||
}
|
||||
|
||||
// Encode implements the ErrorEncoder interface for encoding ValidationErrors
|
||||
func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http.ResponseWriter) {
|
||||
enc.Encoder(ctx, ConvertErrors(err), w)
|
||||
}
|
||||
|
||||
// ConvertErrors converts all errors to the appropriate error format.
|
||||
func ConvertErrors(err error) error {
|
||||
if e, ok := err.(*routers.RouteError); ok {
|
||||
return convertRouteError(e)
|
||||
}
|
||||
|
||||
e, ok := err.(*RequestError)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
var cErr *ValidationError
|
||||
if e.Err == nil {
|
||||
cErr = convertBasicRequestError(e)
|
||||
} else if e.Err == ErrInvalidRequired {
|
||||
cErr = convertErrInvalidRequired(e)
|
||||
} else if e.Err == ErrInvalidEmptyValue {
|
||||
cErr = convertErrInvalidEmptyValue(e)
|
||||
} else if innerErr, ok := e.Err.(*ParseError); ok {
|
||||
cErr = convertParseError(e, innerErr)
|
||||
} else if innerErr, ok := e.Err.(*openapi3.SchemaError); ok {
|
||||
cErr = convertSchemaError(e, innerErr)
|
||||
}
|
||||
|
||||
if cErr != nil {
|
||||
return cErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func convertRouteError(e *routers.RouteError) *ValidationError {
|
||||
status := http.StatusNotFound
|
||||
if e.Error() == routers.ErrMethodNotAllowed.Error() {
|
||||
status = http.StatusMethodNotAllowed
|
||||
}
|
||||
return &ValidationError{Status: status, Title: e.Error()}
|
||||
}
|
||||
|
||||
func convertBasicRequestError(e *RequestError) *ValidationError {
|
||||
if strings.HasPrefix(e.Reason, prefixInvalidCT) {
|
||||
if strings.HasSuffix(e.Reason, `""`) {
|
||||
return &ValidationError{
|
||||
Status: http.StatusUnsupportedMediaType,
|
||||
Title: "header Content-Type is required",
|
||||
}
|
||||
}
|
||||
return &ValidationError{
|
||||
Status: http.StatusUnsupportedMediaType,
|
||||
Title: prefixUnsupportedCT + strings.TrimPrefix(e.Reason, prefixInvalidCT),
|
||||
}
|
||||
}
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: e.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertErrInvalidRequired(e *RequestError) *ValidationError {
|
||||
if e.Err == ErrInvalidRequired && e.Parameter != nil {
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: fmt.Sprintf("parameter %q in %s is required", e.Parameter.Name, e.Parameter.In),
|
||||
}
|
||||
}
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: e.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertErrInvalidEmptyValue(e *RequestError) *ValidationError {
|
||||
if e.Err == ErrInvalidEmptyValue && e.Parameter != nil {
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: fmt.Sprintf("parameter %q in %s is not allowed to be empty", e.Parameter.Name, e.Parameter.In),
|
||||
}
|
||||
}
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: e.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func convertParseError(e *RequestError, innerErr *ParseError) *ValidationError {
|
||||
// We treat path params of the wrong type like a 404 instead of a 400
|
||||
if innerErr.Kind == KindInvalidFormat && e.Parameter != nil && e.Parameter.In == "path" {
|
||||
return &ValidationError{
|
||||
Status: http.StatusNotFound,
|
||||
Title: fmt.Sprintf("resource not found with %q value: %v", e.Parameter.Name, innerErr.Value),
|
||||
}
|
||||
} else if strings.HasPrefix(innerErr.Reason, prefixUnsupportedCT) {
|
||||
return &ValidationError{
|
||||
Status: http.StatusUnsupportedMediaType,
|
||||
Title: innerErr.Reason,
|
||||
}
|
||||
} else if innerErr.RootCause() != nil {
|
||||
if rootErr, ok := innerErr.Cause.(*ParseError); ok &&
|
||||
rootErr.Kind == KindInvalidFormat && e.Parameter.In == "query" {
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: fmt.Sprintf("parameter %q in %s is invalid: %v is %s",
|
||||
e.Parameter.Name, e.Parameter.In, rootErr.Value, rootErr.Reason),
|
||||
}
|
||||
}
|
||||
return &ValidationError{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: innerErr.Reason,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *ValidationError {
|
||||
cErr := &ValidationError{Title: innerErr.Reason}
|
||||
|
||||
// Handle "Origin" error
|
||||
if originErr, ok := innerErr.Origin.(*openapi3.SchemaError); ok {
|
||||
cErr = convertSchemaError(e, originErr)
|
||||
}
|
||||
|
||||
// Add http status code
|
||||
if e.Parameter != nil {
|
||||
cErr.Status = http.StatusBadRequest
|
||||
} else if e.RequestBody != nil {
|
||||
cErr.Status = http.StatusUnprocessableEntity
|
||||
}
|
||||
|
||||
// Add error source
|
||||
if e.Parameter != nil {
|
||||
// We have a JSONPointer in the query param too so need to
|
||||
// make sure 'Parameter' check takes priority over 'Pointer'
|
||||
cErr.Source = &ValidationErrorSource{Parameter: e.Parameter.Name}
|
||||
} else if ptr := innerErr.JSONPointer(); ptr != nil {
|
||||
cErr.Source = &ValidationErrorSource{Pointer: toJSONPointer(ptr)}
|
||||
}
|
||||
|
||||
// Add details on allowed values for enums
|
||||
if innerErr.SchemaField == "enum" {
|
||||
enums := make([]string, 0, len(innerErr.Schema.Enum))
|
||||
for _, enum := range innerErr.Schema.Enum {
|
||||
enums = append(enums, fmt.Sprintf("%v", enum))
|
||||
}
|
||||
cErr.Detail = fmt.Sprintf("value %v at %s must be one of: %s",
|
||||
innerErr.Value,
|
||||
toJSONPointer(innerErr.JSONPointer()),
|
||||
strings.Join(enums, ", "))
|
||||
value := fmt.Sprintf("%v", innerErr.Value)
|
||||
if e.Parameter != nil &&
|
||||
(e.Parameter.Explode == nil || *e.Parameter.Explode) &&
|
||||
(e.Parameter.Style == "" || e.Parameter.Style == "form") &&
|
||||
strings.Contains(value, ",") {
|
||||
parts := strings.Split(value, ",")
|
||||
cErr.Detail = fmt.Sprintf("%s; perhaps you intended '?%s=%s'",
|
||||
cErr.Detail,
|
||||
e.Parameter.Name,
|
||||
strings.Join(parts, "&"+e.Parameter.Name+"="))
|
||||
}
|
||||
}
|
||||
return cErr
|
||||
}
|
||||
|
||||
func toJSONPointer(reversePath []string) string {
|
||||
return "/" + strings.Join(reversePath, "/")
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
|
||||
)
|
||||
|
||||
// AuthenticationFunc allows for custom security requirement validation.
|
||||
// A non-nil error fails authentication according to https://spec.openapis.org/oas/v3.1.0#security-requirement-object
|
||||
// See ValidateSecurityRequirements
|
||||
type AuthenticationFunc func(context.Context, *AuthenticationInput) error
|
||||
|
||||
// NoopAuthenticationFunc is an AuthenticationFunc
|
||||
func NoopAuthenticationFunc(context.Context, *AuthenticationInput) error { return nil }
|
||||
|
||||
var _ AuthenticationFunc = NoopAuthenticationFunc
|
||||
|
||||
type ValidationHandler struct {
|
||||
Handler http.Handler
|
||||
AuthenticationFunc AuthenticationFunc
|
||||
File string
|
||||
ErrorEncoder ErrorEncoder
|
||||
router routers.Router
|
||||
}
|
||||
|
||||
func (h *ValidationHandler) Load() error {
|
||||
loader := openapi3.NewLoader()
|
||||
doc, err := loader.LoadFromFile(h.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := doc.Validate(loader.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
if h.router, err = legacyrouter.NewRouter(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set defaults
|
||||
if h.Handler == nil {
|
||||
h.Handler = http.DefaultServeMux
|
||||
}
|
||||
if h.AuthenticationFunc == nil {
|
||||
h.AuthenticationFunc = NoopAuthenticationFunc
|
||||
}
|
||||
if h.ErrorEncoder == nil {
|
||||
h.ErrorEncoder = DefaultErrorEncoder
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ValidationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if handled := h.before(w, r); handled {
|
||||
return
|
||||
}
|
||||
// TODO: validateResponse
|
||||
h.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Middleware implements gorilla/mux MiddlewareFunc
|
||||
func (h *ValidationHandler) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if handled := h.before(w, r); handled {
|
||||
return
|
||||
}
|
||||
// TODO: validateResponse
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ValidationHandler) before(w http.ResponseWriter, r *http.Request) (handled bool) {
|
||||
if err := h.validateRequest(r); err != nil {
|
||||
h.ErrorEncoder(r.Context(), err, w)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *ValidationHandler) validateRequest(r *http.Request) error {
|
||||
// Find route
|
||||
route, pathParams, err := h.router.FindRoute(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
options := &Options{
|
||||
AuthenticationFunc: h.AuthenticationFunc,
|
||||
}
|
||||
|
||||
// Validate request
|
||||
requestValidationInput := &RequestValidationInput{
|
||||
Request: r,
|
||||
PathParams: pathParams,
|
||||
Route: route,
|
||||
Options: options,
|
||||
}
|
||||
if err = ValidateRequest(r.Context(), requestValidationInput); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package openapi3filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// We didn't want to tie kin-openapi too tightly with go-kit.
|
||||
// This file contains the ErrorEncoder and DefaultErrorEncoder function
|
||||
// borrowed from this project.
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015 Peter Bourgon
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// ErrorEncoder is responsible for encoding an error to the ResponseWriter.
|
||||
// Users are encouraged to use custom ErrorEncoders to encode HTTP errors to
|
||||
// their clients, and will likely want to pass and check for their own error
|
||||
// types. See the example shipping/handling service.
|
||||
type ErrorEncoder func(ctx context.Context, err error, w http.ResponseWriter)
|
||||
|
||||
// StatusCoder is checked by DefaultErrorEncoder. If an error value implements
|
||||
// StatusCoder, the StatusCode will be used when encoding the error. By default,
|
||||
// StatusInternalServerError (500) is used.
|
||||
type StatusCoder interface {
|
||||
StatusCode() int
|
||||
}
|
||||
|
||||
// Headerer is checked by DefaultErrorEncoder. If an error value implements
|
||||
// Headerer, the provided headers will be applied to the response writer, after
|
||||
// the Content-Type is set.
|
||||
type Headerer interface {
|
||||
Headers() http.Header
|
||||
}
|
||||
|
||||
// DefaultErrorEncoder writes the error to the ResponseWriter, by default a
|
||||
// content type of text/plain, a body of the plain text of the error, and a
|
||||
// status code of 500. If the error implements Headerer, the provided headers
|
||||
// will be applied to the response. If the error implements json.Marshaler, and
|
||||
// the marshaling succeeds, a content type of application/json and the JSON
|
||||
// encoded form of the error will be used. If the error implements StatusCoder,
|
||||
// the provided StatusCode will be used instead of 500.
|
||||
func DefaultErrorEncoder(_ context.Context, err error, w http.ResponseWriter) {
|
||||
contentType, body := "text/plain; charset=utf-8", []byte(err.Error())
|
||||
if marshaler, ok := err.(json.Marshaler); ok {
|
||||
if jsonBody, marshalErr := marshaler.MarshalJSON(); marshalErr == nil {
|
||||
contentType, body = "application/json; charset=utf-8", jsonBody
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
if headerer, ok := err.(Headerer); ok {
|
||||
for k, values := range headerer.Headers() {
|
||||
for _, v := range values {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
code := http.StatusInternalServerError
|
||||
if sc, ok := err.(StatusCoder); ok {
|
||||
code = sc.StatusCode()
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
w.Write(body)
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
// Package gorillamux implements a router.
|
||||
//
|
||||
// It differs from the legacy router:
|
||||
// * it provides somewhat granular errors: "path not found", "method not allowed".
|
||||
// * it handles matching routes with extensions (e.g. /books/{id}.json)
|
||||
// * it handles path patterns with a different syntax (e.g. /params/{x}/{y}/{z:.*})
|
||||
package gorillamux
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
)
|
||||
|
||||
var _ routers.Router = &Router{}
|
||||
|
||||
// Router helps link http.Request.s and an OpenAPIv3 spec
|
||||
type Router struct {
|
||||
muxes []routeMux
|
||||
routes []*routers.Route
|
||||
}
|
||||
|
||||
type varsf func(vars map[string]string)
|
||||
|
||||
type routeMux struct {
|
||||
muxRoute *mux.Route
|
||||
varsUpdater varsf
|
||||
}
|
||||
|
||||
type srv struct {
|
||||
schemes []string
|
||||
host, base string
|
||||
server *openapi3.Server
|
||||
varsUpdater varsf
|
||||
}
|
||||
|
||||
var singleVariableMatcher = regexp.MustCompile(`^\{([^{}]+)\}$`)
|
||||
|
||||
// TODO: Handle/HandlerFunc + ServeHTTP (When there is a match, the route variables can be retrieved calling mux.Vars(request))
|
||||
|
||||
// NewRouter creates a gorilla/mux router.
|
||||
// Assumes spec is .Validate()d
|
||||
// Note that a variable for the port number MUST have a default value and only this value will match as the port (see issue #367).
|
||||
func NewRouter(doc *openapi3.T) (routers.Router, error) {
|
||||
servers, err := makeServers(doc.Servers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
muxRouter := mux.NewRouter().UseEncodedPath()
|
||||
r := &Router{}
|
||||
for _, path := range doc.Paths.InMatchingOrder() {
|
||||
pathItem := doc.Paths.Value(path)
|
||||
if len(pathItem.Servers) > 0 {
|
||||
if servers, err = makeServers(pathItem.Servers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
operations := pathItem.Operations()
|
||||
methods := make([]string, 0, len(operations))
|
||||
for method := range operations {
|
||||
methods = append(methods, method)
|
||||
}
|
||||
sort.Strings(methods)
|
||||
|
||||
for _, s := range servers {
|
||||
muxRoute := muxRouter.Path(s.base + path).Methods(methods...)
|
||||
if schemes := s.schemes; len(schemes) != 0 {
|
||||
muxRoute.Schemes(schemes...)
|
||||
}
|
||||
if host := s.host; host != "" {
|
||||
muxRoute.Host(host)
|
||||
}
|
||||
if err := muxRoute.GetError(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.muxes = append(r.muxes, routeMux{
|
||||
muxRoute: muxRoute,
|
||||
varsUpdater: s.varsUpdater,
|
||||
})
|
||||
r.routes = append(r.routes, &routers.Route{
|
||||
Spec: doc,
|
||||
Server: s.server,
|
||||
Path: path,
|
||||
PathItem: pathItem,
|
||||
Method: "",
|
||||
Operation: nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// FindRoute extracts the route and parameters of an http.Request
|
||||
func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) {
|
||||
for i, m := range r.muxes {
|
||||
var match mux.RouteMatch
|
||||
if m.muxRoute.Match(req, &match) {
|
||||
if err := match.MatchErr; err != nil {
|
||||
// What then?
|
||||
}
|
||||
vars := match.Vars
|
||||
if f := m.varsUpdater; f != nil {
|
||||
f(vars)
|
||||
}
|
||||
route := *r.routes[i]
|
||||
route.Method = req.Method
|
||||
route.Operation = route.Spec.Paths.Value(route.Path).GetOperation(route.Method)
|
||||
return &route, vars, nil
|
||||
}
|
||||
switch match.MatchErr {
|
||||
case nil:
|
||||
case mux.ErrMethodMismatch:
|
||||
return nil, nil, routers.ErrMethodNotAllowed
|
||||
default: // What then?
|
||||
}
|
||||
}
|
||||
return nil, nil, routers.ErrPathNotFound
|
||||
}
|
||||
|
||||
func makeServers(in openapi3.Servers) ([]srv, error) {
|
||||
servers := make([]srv, 0, len(in))
|
||||
for _, server := range in {
|
||||
serverURL := server.URL
|
||||
if submatch := singleVariableMatcher.FindStringSubmatch(serverURL); submatch != nil {
|
||||
sVar := submatch[1]
|
||||
sVal := server.Variables[sVar].Default
|
||||
serverURL = strings.ReplaceAll(serverURL, "{"+sVar+"}", sVal)
|
||||
var varsUpdater varsf
|
||||
if lhs := strings.TrimSuffix(serverURL, server.Variables[sVar].Default); lhs != "" {
|
||||
varsUpdater = func(vars map[string]string) { vars[sVar] = lhs }
|
||||
}
|
||||
svr, err := newSrv(serverURL, server, varsUpdater)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
servers = append(servers, svr)
|
||||
continue
|
||||
}
|
||||
|
||||
// If a variable represents the port "http://domain.tld:{port}/bla"
|
||||
// then url.Parse() cannot parse "http://domain.tld:`bEncode({port})`/bla"
|
||||
// and mux is not able to set the {port} variable
|
||||
// So we just use the default value for this variable.
|
||||
// See https://github.com/getkin/kin-openapi/issues/367
|
||||
var varsUpdater varsf
|
||||
if lhs := strings.Index(serverURL, ":{"); lhs > 0 {
|
||||
rest := serverURL[lhs+len(":{"):]
|
||||
rhs := strings.Index(rest, "}")
|
||||
portVariable := rest[:rhs]
|
||||
portValue := server.Variables[portVariable].Default
|
||||
serverURL = strings.ReplaceAll(serverURL, "{"+portVariable+"}", portValue)
|
||||
varsUpdater = func(vars map[string]string) {
|
||||
vars[portVariable] = portValue
|
||||
}
|
||||
}
|
||||
|
||||
svr, err := newSrv(serverURL, server, varsUpdater)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers = append(servers, svr)
|
||||
}
|
||||
if len(servers) == 0 {
|
||||
servers = append(servers, srv{})
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
func newSrv(serverURL string, server *openapi3.Server, varsUpdater varsf) (srv, error) {
|
||||
var schemes []string
|
||||
if strings.Contains(serverURL, "://") {
|
||||
scheme0 := strings.Split(serverURL, "://")[0]
|
||||
schemes = permutePart(scheme0, server)
|
||||
serverURL = strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1)
|
||||
}
|
||||
|
||||
u, err := url.Parse(bEncode(serverURL))
|
||||
if err != nil {
|
||||
return srv{}, err
|
||||
}
|
||||
path := bDecode(u.EscapedPath())
|
||||
if len(path) > 0 && path[len(path)-1] == '/' {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
svr := srv{
|
||||
host: bDecode(u.Host), //u.Hostname()?
|
||||
base: path,
|
||||
schemes: schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624
|
||||
server: server,
|
||||
varsUpdater: varsUpdater,
|
||||
}
|
||||
return svr, nil
|
||||
}
|
||||
|
||||
// Magic strings that temporarily replace "{}" so net/url.Parse() works
|
||||
var blURL, brURL = strings.Repeat("-", 50), strings.Repeat("_", 50)
|
||||
|
||||
func bEncode(s string) string {
|
||||
s = strings.Replace(s, "{", blURL, -1)
|
||||
s = strings.Replace(s, "}", brURL, -1)
|
||||
return s
|
||||
}
|
||||
func bDecode(s string) string {
|
||||
s = strings.Replace(s, blURL, "{", -1)
|
||||
s = strings.Replace(s, brURL, "}", -1)
|
||||
return s
|
||||
}
|
||||
|
||||
func permutePart(part0 string, srv *openapi3.Server) []string {
|
||||
type mapAndSlice struct {
|
||||
m map[string]struct{}
|
||||
s []string
|
||||
}
|
||||
var2val := make(map[string]mapAndSlice)
|
||||
max := 0
|
||||
for name0, v := range srv.Variables {
|
||||
name := "{" + name0 + "}"
|
||||
if !strings.Contains(part0, name) {
|
||||
continue
|
||||
}
|
||||
m := map[string]struct{}{v.Default: {}}
|
||||
for _, value := range v.Enum {
|
||||
m[value] = struct{}{}
|
||||
}
|
||||
if l := len(m); l > max {
|
||||
max = l
|
||||
}
|
||||
s := make([]string, 0, len(m))
|
||||
for value := range m {
|
||||
s = append(s, value)
|
||||
}
|
||||
var2val[name] = mapAndSlice{m: m, s: s}
|
||||
}
|
||||
if len(var2val) == 0 {
|
||||
return []string{part0}
|
||||
}
|
||||
|
||||
partsMap := make(map[string]struct{}, max*len(var2val))
|
||||
for i := 0; i < max; i++ {
|
||||
part := part0
|
||||
for name, mas := range var2val {
|
||||
part = strings.Replace(part, name, mas.s[i%len(mas.s)], -1)
|
||||
}
|
||||
partsMap[part] = struct{}{}
|
||||
}
|
||||
parts := make([]string, 0, len(partsMap))
|
||||
for part := range partsMap {
|
||||
parts = append(parts, part)
|
||||
}
|
||||
sort.Strings(parts)
|
||||
return parts
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
// Package pathpattern implements path matching.
|
||||
//
|
||||
// Examples of supported patterns:
|
||||
// - "/"
|
||||
// - "/abc""
|
||||
// - "/abc/{variable}" (matches until next '/' or end-of-string)
|
||||
// - "/abc/{variable*}" (matches everything, including "/abc" if "/abc" has root)
|
||||
// - "/abc/{ variable | prefix_(.*}_suffix }" (matches regular expressions)
|
||||
package pathpattern
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var DefaultOptions = &Options{
|
||||
SupportWildcard: true,
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
SupportWildcard bool
|
||||
SupportRegExp bool
|
||||
}
|
||||
|
||||
// PathFromHost converts a host pattern to a path pattern.
|
||||
//
|
||||
// Examples:
|
||||
// - PathFromHost("some-subdomain.domain.com", false) -> "com/./domain/./some-subdomain"
|
||||
// - PathFromHost("some-subdomain.domain.com", true) -> "com/./domain/./subdomain/-/some"
|
||||
func PathFromHost(host string, specialDashes bool) string {
|
||||
buf := make([]byte, 0, len(host))
|
||||
end := len(host)
|
||||
|
||||
// Go from end to start
|
||||
for start := end - 1; start >= 0; start-- {
|
||||
switch host[start] {
|
||||
case '.':
|
||||
buf = append(buf, host[start+1:end]...)
|
||||
buf = append(buf, '/', '.', '/')
|
||||
end = start
|
||||
case '-':
|
||||
if specialDashes {
|
||||
buf = append(buf, host[start+1:end]...)
|
||||
buf = append(buf, '/', '-', '/')
|
||||
end = start
|
||||
}
|
||||
}
|
||||
}
|
||||
buf = append(buf, host[:end]...)
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
VariableNames []string
|
||||
Value any
|
||||
Suffixes SuffixList
|
||||
}
|
||||
|
||||
func (currentNode *Node) String() string {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 255))
|
||||
currentNode.toBuffer(buf, "")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (currentNode *Node) toBuffer(buf *bytes.Buffer, linePrefix string) {
|
||||
if value := currentNode.Value; value != nil {
|
||||
buf.WriteString(linePrefix)
|
||||
buf.WriteString("VALUE: ")
|
||||
fmt.Fprint(buf, value)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
suffixes := currentNode.Suffixes
|
||||
if len(suffixes) > 0 {
|
||||
newLinePrefix := linePrefix + " "
|
||||
for _, suffix := range suffixes {
|
||||
buf.WriteString(linePrefix)
|
||||
buf.WriteString("PATTERN: ")
|
||||
buf.WriteString(suffix.String())
|
||||
buf.WriteString("\n")
|
||||
suffix.Node.toBuffer(buf, newLinePrefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SuffixKind int
|
||||
|
||||
// Note that order is important!
|
||||
const (
|
||||
// SuffixKindConstant matches a constant string
|
||||
SuffixKindConstant = SuffixKind(iota)
|
||||
|
||||
// SuffixKindRegExp matches a regular expression
|
||||
SuffixKindRegExp
|
||||
|
||||
// SuffixKindVariable matches everything until '/'
|
||||
SuffixKindVariable
|
||||
|
||||
// SuffixKindEverything matches everything (until end-of-string)
|
||||
SuffixKindEverything
|
||||
)
|
||||
|
||||
// Suffix describes condition that
|
||||
type Suffix struct {
|
||||
Kind SuffixKind
|
||||
Pattern string
|
||||
|
||||
// compiled regular expression
|
||||
regExp *regexp.Regexp
|
||||
|
||||
// Next node
|
||||
Node *Node
|
||||
}
|
||||
|
||||
func EqualSuffix(a, b Suffix) bool {
|
||||
return a.Kind == b.Kind && a.Pattern == b.Pattern
|
||||
}
|
||||
|
||||
func (suffix Suffix) String() string {
|
||||
switch suffix.Kind {
|
||||
case SuffixKindConstant:
|
||||
return suffix.Pattern
|
||||
case SuffixKindVariable:
|
||||
return "{_}"
|
||||
case SuffixKindEverything:
|
||||
return "{_*}"
|
||||
default:
|
||||
return "{_|" + suffix.Pattern + "}"
|
||||
}
|
||||
}
|
||||
|
||||
type SuffixList []Suffix
|
||||
|
||||
func (list SuffixList) Less(i, j int) bool {
|
||||
a, b := list[i], list[j]
|
||||
ak, bk := a.Kind, b.Kind
|
||||
if ak < bk {
|
||||
return true
|
||||
} else if bk < ak {
|
||||
return false
|
||||
}
|
||||
return a.Pattern > b.Pattern
|
||||
}
|
||||
|
||||
func (list SuffixList) Len() int {
|
||||
return len(list)
|
||||
}
|
||||
|
||||
func (list SuffixList) Swap(i, j int) {
|
||||
a, b := list[i], list[j]
|
||||
list[i], list[j] = b, a
|
||||
}
|
||||
|
||||
func (currentNode *Node) MustAdd(path string, value any, options *Options) {
|
||||
node, err := currentNode.CreateNode(path, options)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
node.Value = value
|
||||
}
|
||||
|
||||
func (currentNode *Node) Add(path string, value any, options *Options) error {
|
||||
node, err := currentNode.CreateNode(path, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node.Value = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (currentNode *Node) CreateNode(path string, options *Options) (*Node, error) {
|
||||
if options == nil {
|
||||
options = DefaultOptions
|
||||
}
|
||||
for strings.HasSuffix(path, "/") {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
remaining := path
|
||||
var variableNames []string
|
||||
loop:
|
||||
for {
|
||||
//remaining = strings.TrimPrefix(remaining, "/")
|
||||
if len(remaining) == 0 {
|
||||
// This node is the right one
|
||||
// Check whether another route already leads to this node
|
||||
currentNode.VariableNames = variableNames
|
||||
return currentNode, nil
|
||||
}
|
||||
|
||||
suffix := Suffix{}
|
||||
var i int
|
||||
if strings.HasPrefix(remaining, "/") {
|
||||
remaining = remaining[1:]
|
||||
suffix.Kind = SuffixKindConstant
|
||||
suffix.Pattern = "/"
|
||||
} else {
|
||||
i = strings.IndexAny(remaining, "/{")
|
||||
if i < 0 {
|
||||
i = len(remaining)
|
||||
}
|
||||
if i > 0 {
|
||||
// Constant string pattern
|
||||
suffix.Kind = SuffixKindConstant
|
||||
suffix.Pattern = remaining[:i]
|
||||
remaining = remaining[i:]
|
||||
} else if remaining[0] == '{' {
|
||||
// This is probably a variable
|
||||
suffix.Kind = SuffixKindVariable
|
||||
|
||||
// Find variable name
|
||||
i := strings.IndexByte(remaining, '}')
|
||||
if i < 0 {
|
||||
return nil, fmt.Errorf("missing '}' in: %s", path)
|
||||
}
|
||||
variableName := strings.TrimSpace(remaining[1:i])
|
||||
remaining = remaining[i+1:]
|
||||
|
||||
if options.SupportRegExp {
|
||||
// See if it has regular expression
|
||||
i = strings.IndexByte(variableName, '|')
|
||||
if i >= 0 {
|
||||
suffix.Kind = SuffixKindRegExp
|
||||
suffix.Pattern = strings.TrimSpace(variableName[i+1:])
|
||||
variableName = strings.TrimSpace(variableName[:i])
|
||||
}
|
||||
}
|
||||
if suffix.Kind == SuffixKindVariable && options.SupportWildcard {
|
||||
if strings.HasSuffix(variableName, "*") {
|
||||
suffix.Kind = SuffixKindEverything
|
||||
}
|
||||
}
|
||||
variableNames = append(variableNames, variableName)
|
||||
}
|
||||
}
|
||||
|
||||
// Find existing matcher
|
||||
for _, existing := range currentNode.Suffixes {
|
||||
if EqualSuffix(existing, suffix) {
|
||||
currentNode = existing.Node
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
// Compile regular expression
|
||||
if suffix.Kind == SuffixKindRegExp {
|
||||
regExp, err := regexp.Compile(suffix.Pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid regular expression in: %s", path)
|
||||
}
|
||||
suffix.regExp = regExp
|
||||
}
|
||||
|
||||
// Create new node
|
||||
newNode := &Node{}
|
||||
suffix.Node = newNode
|
||||
currentNode.Suffixes = append(currentNode.Suffixes, suffix)
|
||||
sort.Sort(currentNode.Suffixes)
|
||||
currentNode = newNode
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
func (currentNode *Node) Match(path string) (*Node, []string) {
|
||||
for strings.HasSuffix(path, "/") {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
variableValues := make([]string, 0, 8)
|
||||
return currentNode.matchRemaining(path, variableValues)
|
||||
}
|
||||
|
||||
func (currentNode *Node) matchRemaining(remaining string, paramValues []string) (*Node, []string) {
|
||||
// Check if this node matches
|
||||
if len(remaining) == 0 && currentNode.Value != nil {
|
||||
return currentNode, paramValues
|
||||
}
|
||||
|
||||
// See if any suffix matches
|
||||
for _, suffix := range currentNode.Suffixes {
|
||||
var resultNode *Node
|
||||
var resultValues []string
|
||||
switch suffix.Kind {
|
||||
case SuffixKindConstant:
|
||||
pattern := suffix.Pattern
|
||||
if strings.HasPrefix(remaining, pattern) {
|
||||
newRemaining := remaining[len(pattern):]
|
||||
resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, paramValues)
|
||||
} else if len(remaining) == 0 && pattern == "/" {
|
||||
resultNode, resultValues = suffix.Node.matchRemaining(remaining, paramValues)
|
||||
}
|
||||
case SuffixKindVariable:
|
||||
i := strings.IndexByte(remaining, '/')
|
||||
if i < 0 {
|
||||
i = len(remaining)
|
||||
}
|
||||
newParamValues := append(paramValues, remaining[:i])
|
||||
newRemaining := remaining[i:]
|
||||
resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, newParamValues)
|
||||
case SuffixKindEverything:
|
||||
newParamValues := append(paramValues, remaining)
|
||||
resultNode, resultValues = suffix.Node, newParamValues
|
||||
case SuffixKindRegExp:
|
||||
i := strings.IndexByte(remaining, '/')
|
||||
if i < 0 {
|
||||
i = len(remaining)
|
||||
}
|
||||
paramValue := remaining[:i]
|
||||
regExp := suffix.regExp
|
||||
if regExp.MatchString(paramValue) {
|
||||
matches := regExp.FindStringSubmatch(paramValue)
|
||||
if len(matches) > 1 {
|
||||
paramValue = matches[1]
|
||||
}
|
||||
newParamValues := append(paramValues, paramValue)
|
||||
newRemaining := remaining[i:]
|
||||
resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, newParamValues)
|
||||
}
|
||||
}
|
||||
if resultNode != nil && resultNode.Value != nil {
|
||||
// This suffix matched
|
||||
return resultNode, resultValues
|
||||
}
|
||||
}
|
||||
|
||||
// No suffix matched
|
||||
return nil, nil
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// Package legacy implements a router.
|
||||
//
|
||||
// It differs from the gorilla/mux router:
|
||||
// * it provides granular errors: "path not found", "method not allowed", "variable missing from path"
|
||||
// * it does not handle matching routes with extensions (e.g. /books/{id}.json)
|
||||
// * it handles path patterns with a different syntax (e.g. /params/{x}/{y}/{z.*})
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
"github.com/getkin/kin-openapi/routers/legacy/pathpattern"
|
||||
)
|
||||
|
||||
// Routers maps a HTTP request to a Router.
|
||||
type Routers []*Router
|
||||
|
||||
// FindRoute extracts the route and parameters of an http.Request
|
||||
func (rs Routers) FindRoute(req *http.Request) (routers.Router, *routers.Route, map[string]string, error) {
|
||||
for _, router := range rs {
|
||||
// Skip routers that have DO NOT have servers
|
||||
if len(router.doc.Servers) == 0 {
|
||||
continue
|
||||
}
|
||||
route, pathParams, err := router.FindRoute(req)
|
||||
if err == nil {
|
||||
return router, route, pathParams, nil
|
||||
}
|
||||
}
|
||||
for _, router := range rs {
|
||||
// Skip routers that DO have servers
|
||||
if len(router.doc.Servers) > 0 {
|
||||
continue
|
||||
}
|
||||
route, pathParams, err := router.FindRoute(req)
|
||||
if err == nil {
|
||||
return router, route, pathParams, nil
|
||||
}
|
||||
}
|
||||
return nil, nil, nil, &routers.RouteError{
|
||||
Reason: "none of the routers match",
|
||||
}
|
||||
}
|
||||
|
||||
// Router maps a HTTP request to an OpenAPI operation.
|
||||
type Router struct {
|
||||
doc *openapi3.T
|
||||
pathNode *pathpattern.Node
|
||||
}
|
||||
|
||||
// NewRouter creates a new router.
|
||||
//
|
||||
// If the given OpenAPIv3 document has servers, router will use them.
|
||||
// All operations of the document will be added to the router.
|
||||
func NewRouter(doc *openapi3.T, opts ...openapi3.ValidationOption) (routers.Router, error) {
|
||||
if err := doc.Validate(context.Background(), opts...); err != nil {
|
||||
return nil, fmt.Errorf("validating OpenAPI failed: %w", err)
|
||||
}
|
||||
router := &Router{doc: doc}
|
||||
root := router.node()
|
||||
for path, pathItem := range doc.Paths.Map() {
|
||||
for method, operation := range pathItem.Operations() {
|
||||
method = strings.ToUpper(method)
|
||||
if err := root.Add(method+" "+path, &routers.Route{
|
||||
Spec: doc,
|
||||
Path: path,
|
||||
PathItem: pathItem,
|
||||
Method: method,
|
||||
Operation: operation,
|
||||
}, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return router, nil
|
||||
}
|
||||
|
||||
// AddRoute adds a route in the router.
|
||||
func (router *Router) AddRoute(route *routers.Route) error {
|
||||
method := route.Method
|
||||
if method == "" {
|
||||
return errors.New("route is missing method")
|
||||
}
|
||||
method = strings.ToUpper(method)
|
||||
path := route.Path
|
||||
if path == "" {
|
||||
return errors.New("route is missing path")
|
||||
}
|
||||
return router.node().Add(method+" "+path, router, nil)
|
||||
}
|
||||
|
||||
func (router *Router) node() *pathpattern.Node {
|
||||
root := router.pathNode
|
||||
if root == nil {
|
||||
root = &pathpattern.Node{}
|
||||
router.pathNode = root
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// FindRoute extracts the route and parameters of an http.Request
|
||||
func (router *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) {
|
||||
method, url := req.Method, req.URL
|
||||
doc := router.doc
|
||||
|
||||
// Get server
|
||||
servers := doc.Servers
|
||||
var server *openapi3.Server
|
||||
var remainingPath string
|
||||
var pathParams map[string]string
|
||||
if len(servers) == 0 {
|
||||
remainingPath = url.Path
|
||||
} else {
|
||||
var paramValues []string
|
||||
server, paramValues, remainingPath = servers.MatchURL(url)
|
||||
if server == nil {
|
||||
return nil, nil, &routers.RouteError{
|
||||
Reason: routers.ErrPathNotFound.Error(),
|
||||
}
|
||||
}
|
||||
pathParams = make(map[string]string)
|
||||
paramNames, err := server.ParameterNames()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for i, value := range paramValues {
|
||||
name := paramNames[i]
|
||||
pathParams[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Get PathItem
|
||||
root := router.node()
|
||||
var route *routers.Route
|
||||
node, paramValues := root.Match(method + " " + remainingPath)
|
||||
if node != nil {
|
||||
route, _ = node.Value.(*routers.Route)
|
||||
}
|
||||
if route == nil {
|
||||
pathItem := doc.Paths.Value(remainingPath)
|
||||
if pathItem == nil {
|
||||
return nil, nil, &routers.RouteError{Reason: routers.ErrPathNotFound.Error()}
|
||||
}
|
||||
if pathItem.GetOperation(method) == nil {
|
||||
return nil, nil, &routers.RouteError{Reason: routers.ErrMethodNotAllowed.Error()}
|
||||
}
|
||||
}
|
||||
|
||||
if pathParams == nil {
|
||||
pathParams = make(map[string]string, len(paramValues))
|
||||
}
|
||||
paramKeys := node.VariableNames
|
||||
for i, value := range paramValues {
|
||||
key := strings.TrimSuffix(paramKeys[i], "*")
|
||||
pathParams[key] = value
|
||||
}
|
||||
return route, pathParams, nil
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
// Router helps link http.Request.s and an OpenAPIv3 spec
|
||||
type Router interface {
|
||||
// FindRoute matches an HTTP request with the operation it resolves to.
|
||||
// Hosts are matched from the OpenAPIv3 servers key.
|
||||
//
|
||||
// If you experience ErrPathNotFound and have localhost hosts specified as your servers,
|
||||
// turning these server URLs as relative (leaving only the path) should resolve this.
|
||||
//
|
||||
// See openapi3filter for example uses with request and response validation.
|
||||
FindRoute(req *http.Request) (route *Route, pathParams map[string]string, err error)
|
||||
}
|
||||
|
||||
// Route describes the operation an http.Request can match
|
||||
type Route struct {
|
||||
Spec *openapi3.T
|
||||
Server *openapi3.Server
|
||||
Path string
|
||||
PathItem *openapi3.PathItem
|
||||
Method string
|
||||
Operation *openapi3.Operation
|
||||
}
|
||||
|
||||
// ErrPathNotFound is returned when no route match is found
|
||||
var ErrPathNotFound error = &RouteError{"no matching operation was found"}
|
||||
|
||||
// ErrMethodNotAllowed is returned when no method of the matched route matches
|
||||
var ErrMethodNotAllowed error = &RouteError{"method not allowed"}
|
||||
|
||||
// RouteError describes Router errors
|
||||
type RouteError struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (e *RouteError) Error() string { return e.Reason }
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
; https://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
eclint_indent_style = unset
|
||||
Generated
+1
@@ -0,0 +1 @@
|
||||
coverage.coverprofile
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2023 The Gorilla Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '')
|
||||
GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
GO_SEC=$(shell which gosec 2> /dev/null || echo '')
|
||||
GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
|
||||
GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '')
|
||||
GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
||||
.PHONY: golangci-lint
|
||||
golangci-lint:
|
||||
$(if $(GO_LINT), ,go install $(GO_LINT_URI))
|
||||
@echo "##### Running golangci-lint"
|
||||
golangci-lint run -v
|
||||
|
||||
.PHONY: gosec
|
||||
gosec:
|
||||
$(if $(GO_SEC), ,go install $(GO_SEC_URI))
|
||||
@echo "##### Running gosec"
|
||||
gosec ./...
|
||||
|
||||
.PHONY: govulncheck
|
||||
govulncheck:
|
||||
$(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI))
|
||||
@echo "##### Running govulncheck"
|
||||
govulncheck ./...
|
||||
|
||||
.PHONY: verify
|
||||
verify: golangci-lint gosec govulncheck
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@echo "##### Running tests"
|
||||
go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./...
|
||||
+812
@@ -0,0 +1,812 @@
|
||||
# gorilla/mux
|
||||
|
||||

|
||||
[](https://codecov.io/github/gorilla/mux)
|
||||
[](https://godoc.org/github.com/gorilla/mux)
|
||||
[](https://sourcegraph.com/github.com/gorilla/mux?badge)
|
||||
|
||||
|
||||

|
||||
|
||||
Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to
|
||||
their respective handler.
|
||||
|
||||
The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are:
|
||||
|
||||
* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`.
|
||||
* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers.
|
||||
* URL hosts, paths and query values can have variables with an optional regular expression.
|
||||
* Registered URLs can be built, or "reversed", which helps maintaining references to resources.
|
||||
* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching.
|
||||
|
||||
---
|
||||
|
||||
* [Install](#install)
|
||||
* [Examples](#examples)
|
||||
* [Matching Routes](#matching-routes)
|
||||
* [Static Files](#static-files)
|
||||
* [Serving Single Page Applications](#serving-single-page-applications) (e.g. React, Vue, Ember.js, etc.)
|
||||
* [Registered URLs](#registered-urls)
|
||||
* [Walking Routes](#walking-routes)
|
||||
* [Graceful Shutdown](#graceful-shutdown)
|
||||
* [Middleware](#middleware)
|
||||
* [Handling CORS Requests](#handling-cors-requests)
|
||||
* [Testing Handlers](#testing-handlers)
|
||||
* [Full Example](#full-example)
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain:
|
||||
|
||||
```sh
|
||||
go get -u github.com/gorilla/mux
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Let's start registering a couple of URL paths and handlers:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", HomeHandler)
|
||||
r.HandleFunc("/products", ProductsHandler)
|
||||
r.HandleFunc("/articles", ArticlesHandler)
|
||||
http.Handle("/", r)
|
||||
}
|
||||
```
|
||||
|
||||
Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters.
|
||||
|
||||
Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/products/{key}", ProductHandler)
|
||||
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
|
||||
```
|
||||
|
||||
The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`:
|
||||
|
||||
```go
|
||||
func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, "Category: %v\n", vars["category"])
|
||||
}
|
||||
```
|
||||
|
||||
And this is all you need to know about the basic usage. More advanced options are explained below.
|
||||
|
||||
### Matching Routes
|
||||
|
||||
Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
// Only matches if domain is "www.example.com".
|
||||
r.Host("www.example.com")
|
||||
// Matches a dynamic subdomain.
|
||||
r.Host("{subdomain:[a-z]+}.example.com")
|
||||
```
|
||||
|
||||
There are several other matchers that can be added. To match path prefixes:
|
||||
|
||||
```go
|
||||
r.PathPrefix("/products/")
|
||||
```
|
||||
|
||||
...or HTTP methods:
|
||||
|
||||
```go
|
||||
r.Methods("GET", "POST")
|
||||
```
|
||||
|
||||
...or URL schemes:
|
||||
|
||||
```go
|
||||
r.Schemes("https")
|
||||
```
|
||||
|
||||
...or header values:
|
||||
|
||||
```go
|
||||
r.Headers("X-Requested-With", "XMLHttpRequest")
|
||||
```
|
||||
|
||||
...or query values:
|
||||
|
||||
```go
|
||||
r.Queries("key", "value")
|
||||
```
|
||||
|
||||
...or to use a custom matcher function:
|
||||
|
||||
```go
|
||||
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
|
||||
return r.ProtoMajor == 0
|
||||
})
|
||||
```
|
||||
|
||||
...and finally, it is possible to combine several matchers in a single route:
|
||||
|
||||
```go
|
||||
r.HandleFunc("/products", ProductsHandler).
|
||||
Host("www.example.com").
|
||||
Methods("GET").
|
||||
Schemes("http")
|
||||
```
|
||||
|
||||
Routes are tested in the order they were added to the router. If two routes match, the first one wins:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/specific", specificHandler)
|
||||
r.PathPrefix("/").Handler(catchAllHandler)
|
||||
```
|
||||
|
||||
Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting".
|
||||
|
||||
For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("www.example.com").Subrouter()
|
||||
```
|
||||
|
||||
Then register routes in the subrouter:
|
||||
|
||||
```go
|
||||
s.HandleFunc("/products/", ProductsHandler)
|
||||
s.HandleFunc("/products/{key}", ProductHandler)
|
||||
s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
|
||||
```
|
||||
|
||||
The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route.
|
||||
|
||||
Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter.
|
||||
|
||||
There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.PathPrefix("/products").Subrouter()
|
||||
// "/products/"
|
||||
s.HandleFunc("/", ProductsHandler)
|
||||
// "/products/{key}/"
|
||||
s.HandleFunc("/{key}/", ProductHandler)
|
||||
// "/products/{key}/details"
|
||||
s.HandleFunc("/{key}/details", ProductDetailsHandler)
|
||||
```
|
||||
|
||||
|
||||
### Static Files
|
||||
|
||||
Note that the path provided to `PathPrefix()` represents a "wildcard": calling
|
||||
`PathPrefix("/static/").Handler(...)` means that the handler will be passed any
|
||||
request that matches "/static/\*". This makes it easy to serve static files with mux:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
var dir string
|
||||
|
||||
flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir")
|
||||
flag.Parse()
|
||||
r := mux.NewRouter()
|
||||
|
||||
// This will serve files under http://localhost:8000/static/<filename>
|
||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir))))
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: r,
|
||||
Addr: "127.0.0.1:8000",
|
||||
// Good practice: enforce timeouts for servers you create!
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
```
|
||||
|
||||
### Serving Single Page Applications
|
||||
|
||||
Most of the time it makes sense to serve your SPA on a separate web server from your API,
|
||||
but sometimes it's desirable to serve them both from one place. It's possible to write a simple
|
||||
handler for serving your SPA (for use with React Router's [BrowserRouter](https://reacttraining.com/react-router/web/api/BrowserRouter) for example), and leverage
|
||||
mux's powerful routing for your API endpoints.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// spaHandler implements the http.Handler interface, so we can use it
|
||||
// to respond to HTTP requests. The path to the static directory and
|
||||
// path to the index file within that static directory are used to
|
||||
// serve the SPA in the given static directory.
|
||||
type spaHandler struct {
|
||||
staticPath string
|
||||
indexPath string
|
||||
}
|
||||
|
||||
// ServeHTTP inspects the URL path to locate a file within the static dir
|
||||
// on the SPA handler. If a file is found, it will be served. If not, the
|
||||
// file located at the index path on the SPA handler will be served. This
|
||||
// is suitable behavior for serving an SPA (single page application).
|
||||
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Join internally call path.Clean to prevent directory traversal
|
||||
path := filepath.Join(h.staticPath, r.URL.Path)
|
||||
|
||||
// check whether a file exists or is a directory at the given path
|
||||
fi, err := os.Stat(path)
|
||||
if os.IsNotExist(err) || fi.IsDir() {
|
||||
// file does not exist or path is a directory, serve index.html
|
||||
http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath))
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// if we got an error (that wasn't that the file doesn't exist) stating the
|
||||
// file, return a 500 internal server error and stop
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, use http.FileServer to serve the static file
|
||||
http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func main() {
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
// an example API handler
|
||||
json.NewEncoder(w).Encode(map[string]bool{"ok": true})
|
||||
})
|
||||
|
||||
spa := spaHandler{staticPath: "build", indexPath: "index.html"}
|
||||
router.PathPrefix("/").Handler(spa)
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: router,
|
||||
Addr: "127.0.0.1:8000",
|
||||
// Good practice: enforce timeouts for servers you create!
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
```
|
||||
|
||||
### Registered URLs
|
||||
|
||||
Now let's see how to build registered URLs.
|
||||
|
||||
Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
Name("article")
|
||||
```
|
||||
|
||||
To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do:
|
||||
|
||||
```go
|
||||
url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
```
|
||||
|
||||
...and the result will be a `url.URL` with the following path:
|
||||
|
||||
```
|
||||
"/articles/technology/42"
|
||||
```
|
||||
|
||||
This also works for host and query value variables:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.Host("{subdomain}.example.com").
|
||||
Path("/articles/{category}/{id:[0-9]+}").
|
||||
Queries("filter", "{filter}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// url.String() will be "http://news.example.com/articles/technology/42?filter=gorilla"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42",
|
||||
"filter", "gorilla")
|
||||
```
|
||||
|
||||
All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match.
|
||||
|
||||
Regex support also exists for matching Headers within a route. For example, we could do:
|
||||
|
||||
```go
|
||||
r.HeadersRegexp("Content-Type", "application/(text|json)")
|
||||
```
|
||||
|
||||
...and the route will match both requests with a Content-Type of `application/json` as well as `application/text`
|
||||
|
||||
There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do:
|
||||
|
||||
```go
|
||||
// "http://news.example.com/"
|
||||
host, err := r.Get("article").URLHost("subdomain", "news")
|
||||
|
||||
// "/articles/technology/42"
|
||||
path, err := r.Get("article").URLPath("category", "technology", "id", "42")
|
||||
```
|
||||
|
||||
And if you use subrouters, host and path defined separately can be built as well:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("{subdomain}.example.com").Subrouter()
|
||||
s.Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// "http://news.example.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
```
|
||||
|
||||
To find all the required variables for a given route when calling `URL()`, the method `GetVarNames()` is available:
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.Host("{domain}").
|
||||
Path("/{group}/{item_id}").
|
||||
Queries("some_data1", "{some_data1}").
|
||||
Queries("some_data2", "{some_data2}").
|
||||
Name("article")
|
||||
|
||||
// Will print [domain group item_id some_data1 some_data2] <nil>
|
||||
fmt.Println(r.Get("article").GetVarNames())
|
||||
|
||||
```
|
||||
### Walking Routes
|
||||
|
||||
The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example,
|
||||
the following prints all of the registered routes:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", handler)
|
||||
r.HandleFunc("/products", handler).Methods("POST")
|
||||
r.HandleFunc("/articles", handler).Methods("GET")
|
||||
r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT")
|
||||
r.HandleFunc("/authors", handler).Queries("surname", "{surname}")
|
||||
err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
||||
pathTemplate, err := route.GetPathTemplate()
|
||||
if err == nil {
|
||||
fmt.Println("ROUTE:", pathTemplate)
|
||||
}
|
||||
pathRegexp, err := route.GetPathRegexp()
|
||||
if err == nil {
|
||||
fmt.Println("Path regexp:", pathRegexp)
|
||||
}
|
||||
queriesTemplates, err := route.GetQueriesTemplates()
|
||||
if err == nil {
|
||||
fmt.Println("Queries templates:", strings.Join(queriesTemplates, ","))
|
||||
}
|
||||
queriesRegexps, err := route.GetQueriesRegexp()
|
||||
if err == nil {
|
||||
fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ","))
|
||||
}
|
||||
methods, err := route.GetMethods()
|
||||
if err == nil {
|
||||
fmt.Println("Methods:", strings.Join(methods, ","))
|
||||
}
|
||||
fmt.Println()
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
http.Handle("/", r)
|
||||
}
|
||||
```
|
||||
|
||||
### Graceful Shutdown
|
||||
|
||||
Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var wait time.Duration
|
||||
flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m")
|
||||
flag.Parse()
|
||||
|
||||
r := mux.NewRouter()
|
||||
// Add your routes as needed
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: "0.0.0.0:8080",
|
||||
// Good practice to set timeouts to avoid Slowloris attacks.
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
Handler: r, // Pass our instance of gorilla/mux in.
|
||||
}
|
||||
|
||||
// Run our server in a goroutine so that it doesn't block.
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
||||
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
||||
signal.Notify(c, os.Interrupt)
|
||||
|
||||
// Block until we receive our signal.
|
||||
<-c
|
||||
|
||||
// Create a deadline to wait for.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), wait)
|
||||
defer cancel()
|
||||
// Doesn't block if no connections, but will otherwise wait
|
||||
// until the timeout deadline.
|
||||
srv.Shutdown(ctx)
|
||||
// Optionally, you could run srv.Shutdown in a goroutine and block on
|
||||
// <-ctx.Done() if your application should wait for other services
|
||||
// to finalize based on context cancellation.
|
||||
log.Println("shutting down")
|
||||
os.Exit(0)
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
Mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed in the order they are added if a match is found, including its subrouters.
|
||||
Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or `ResponseWriter` hijacking.
|
||||
|
||||
Mux middlewares are defined using the de facto standard type:
|
||||
|
||||
```go
|
||||
type MiddlewareFunc func(http.Handler) http.Handler
|
||||
```
|
||||
|
||||
Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc. This takes advantage of closures being able access variables from the context where they are created, while retaining the signature enforced by the receivers.
|
||||
|
||||
A very basic middleware which logs the URI of the request being handled could be written as:
|
||||
|
||||
```go
|
||||
func loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Do stuff here
|
||||
log.Println(r.RequestURI)
|
||||
// Call the next handler, which can be another middleware in the chain, or the final handler.
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Middlewares can be added to a router using `Router.Use()`:
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", handler)
|
||||
r.Use(loggingMiddleware)
|
||||
```
|
||||
|
||||
A more complex authentication middleware, which maps session token to users, could be written as:
|
||||
|
||||
```go
|
||||
// Define our struct
|
||||
type authenticationMiddleware struct {
|
||||
tokenUsers map[string]string
|
||||
}
|
||||
|
||||
// Initialize it somewhere
|
||||
func (amw *authenticationMiddleware) Populate() {
|
||||
amw.tokenUsers["00000000"] = "user0"
|
||||
amw.tokenUsers["aaaaaaaa"] = "userA"
|
||||
amw.tokenUsers["05f717e5"] = "randomUser"
|
||||
amw.tokenUsers["deadbeef"] = "user0"
|
||||
}
|
||||
|
||||
// Middleware function, which will be called for each request
|
||||
func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("X-Session-Token")
|
||||
|
||||
if user, found := amw.tokenUsers[token]; found {
|
||||
// We found the token in our map
|
||||
log.Printf("Authenticated user %s\n", user)
|
||||
// Pass down the request to the next middleware (or final handler)
|
||||
next.ServeHTTP(w, r)
|
||||
} else {
|
||||
// Write an error and stop the handler chain
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", handler)
|
||||
|
||||
amw := authenticationMiddleware{tokenUsers: make(map[string]string)}
|
||||
amw.Populate()
|
||||
|
||||
r.Use(amw.Middleware)
|
||||
```
|
||||
|
||||
Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it.
|
||||
|
||||
### Handling CORS Requests
|
||||
|
||||
[CORSMethodMiddleware](https://godoc.org/github.com/gorilla/mux#CORSMethodMiddleware) intends to make it easier to strictly set the `Access-Control-Allow-Methods` response header.
|
||||
|
||||
* You will still need to use your own CORS handler to set the other CORS headers such as `Access-Control-Allow-Origin`
|
||||
* The middleware will set the `Access-Control-Allow-Methods` header to all the method matchers (e.g. `r.Methods(http.MethodGet, http.MethodPut, http.MethodOptions)` -> `Access-Control-Allow-Methods: GET,PUT,OPTIONS`) on a route
|
||||
* If you do not specify any methods, then:
|
||||
> _Important_: there must be an `OPTIONS` method matcher for the middleware to set the headers.
|
||||
|
||||
Here is an example of using `CORSMethodMiddleware` along with a custom `OPTIONS` handler to set all the required CORS headers:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
|
||||
// IMPORTANT: you must specify an OPTIONS method matcher for the middleware to set CORS headers
|
||||
r.HandleFunc("/foo", fooHandler).Methods(http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodOptions)
|
||||
r.Use(mux.CORSMethodMiddleware(r))
|
||||
|
||||
http.ListenAndServe(":8080", r)
|
||||
}
|
||||
|
||||
func fooHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
if r.Method == http.MethodOptions {
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte("foo"))
|
||||
}
|
||||
```
|
||||
|
||||
And an request to `/foo` using something like:
|
||||
|
||||
```bash
|
||||
curl localhost:8080/foo -v
|
||||
```
|
||||
|
||||
Would look like:
|
||||
|
||||
```bash
|
||||
* Trying ::1...
|
||||
* TCP_NODELAY set
|
||||
* Connected to localhost (::1) port 8080 (#0)
|
||||
> GET /foo HTTP/1.1
|
||||
> Host: localhost:8080
|
||||
> User-Agent: curl/7.59.0
|
||||
> Accept: */*
|
||||
>
|
||||
< HTTP/1.1 200 OK
|
||||
< Access-Control-Allow-Methods: GET,PUT,PATCH,OPTIONS
|
||||
< Access-Control-Allow-Origin: *
|
||||
< Date: Fri, 28 Jun 2019 20:13:30 GMT
|
||||
< Content-Length: 3
|
||||
< Content-Type: text/plain; charset=utf-8
|
||||
<
|
||||
* Connection #0 to host localhost left intact
|
||||
foo
|
||||
```
|
||||
|
||||
### Testing Handlers
|
||||
|
||||
Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_.
|
||||
|
||||
First, our simple HTTP handler:
|
||||
|
||||
```go
|
||||
// endpoints.go
|
||||
package main
|
||||
|
||||
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// A very simple health check.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// In the future we could report back on the status of our DB, or our cache
|
||||
// (e.g. Redis) by performing a simple PING, and include them in the response.
|
||||
io.WriteString(w, `{"alive": true}`)
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/health", HealthCheckHandler)
|
||||
|
||||
log.Fatal(http.ListenAndServe("localhost:8080", r))
|
||||
}
|
||||
```
|
||||
|
||||
Our test code:
|
||||
|
||||
```go
|
||||
// endpoints_test.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHealthCheckHandler(t *testing.T) {
|
||||
// Create a request to pass to our handler. We don't have any query parameters for now, so we'll
|
||||
// pass 'nil' as the third parameter.
|
||||
req, err := http.NewRequest("GET", "/health", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(HealthCheckHandler)
|
||||
|
||||
// Our handlers satisfy http.Handler, so we can call their ServeHTTP method
|
||||
// directly and pass in our Request and ResponseRecorder.
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Check the status code is what we expect.
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
|
||||
// Check the response body is what we expect.
|
||||
expected := `{"alive": true}`
|
||||
if rr.Body.String() != expected {
|
||||
t.Errorf("handler returned unexpected body: got %v want %v",
|
||||
rr.Body.String(), expected)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the case that our routes have [variables](#examples), we can pass those in the request. We could write
|
||||
[table-driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) to test multiple
|
||||
possible route variables as needed.
|
||||
|
||||
```go
|
||||
// endpoints.go
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
// A route with a route variable:
|
||||
r.HandleFunc("/metrics/{type}", MetricsHandler)
|
||||
|
||||
log.Fatal(http.ListenAndServe("localhost:8080", r))
|
||||
}
|
||||
```
|
||||
|
||||
Our test file, with a table-driven test of `routeVariables`:
|
||||
|
||||
```go
|
||||
// endpoints_test.go
|
||||
func TestMetricsHandler(t *testing.T) {
|
||||
tt := []struct{
|
||||
routeVariable string
|
||||
shouldPass bool
|
||||
}{
|
||||
{"goroutines", true},
|
||||
{"heap", true},
|
||||
{"counters", true},
|
||||
{"queries", true},
|
||||
{"adhadaeqm3k", false},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
path := fmt.Sprintf("/metrics/%s", tc.routeVariable)
|
||||
req, err := http.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// To add the vars to the context,
|
||||
// we need to create a router through which we can pass the request.
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/metrics/{type}", MetricsHandler)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
// In this case, our MetricsHandler returns a non-200 response
|
||||
// for a route variable it doesn't know about.
|
||||
if rr.Code == http.StatusOK && !tc.shouldPass {
|
||||
t.Errorf("handler should have failed on routeVariable %s: got %v want %v",
|
||||
tc.routeVariable, rr.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Full Example
|
||||
|
||||
Here's a complete, runnable example of a small `mux` based server:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"log"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func YourHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("Gorilla!\n"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
// Routes consist of a path and a handler function.
|
||||
r.HandleFunc("/", YourHandler)
|
||||
|
||||
// Bind to a port and pass our router in
|
||||
log.Fatal(http.ListenAndServe(":8000", r))
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
BSD licensed. See the LICENSE file for details.
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package mux implements a request router and dispatcher.
|
||||
|
||||
The name mux stands for "HTTP request multiplexer". Like the standard
|
||||
http.ServeMux, mux.Router matches incoming requests against a list of
|
||||
registered routes and calls a handler for the route that matches the URL
|
||||
or other conditions. The main features are:
|
||||
|
||||
- Requests can be matched based on URL host, path, path prefix, schemes,
|
||||
header and query values, HTTP methods or using custom matchers.
|
||||
- URL hosts, paths and query values can have variables with an optional
|
||||
regular expression.
|
||||
- Registered URLs can be built, or "reversed", which helps maintaining
|
||||
references to resources.
|
||||
- Routes can be used as subrouters: nested routes are only tested if the
|
||||
parent route matches. This is useful to define groups of routes that
|
||||
share common conditions like a host, a path prefix or other repeated
|
||||
attributes. As a bonus, this optimizes request matching.
|
||||
- It implements the http.Handler interface so it is compatible with the
|
||||
standard http.ServeMux.
|
||||
|
||||
Let's start registering a couple of URL paths and handlers:
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", HomeHandler)
|
||||
r.HandleFunc("/products", ProductsHandler)
|
||||
r.HandleFunc("/articles", ArticlesHandler)
|
||||
http.Handle("/", r)
|
||||
}
|
||||
|
||||
Here we register three routes mapping URL paths to handlers. This is
|
||||
equivalent to how http.HandleFunc() works: if an incoming request URL matches
|
||||
one of the paths, the corresponding handler is called passing
|
||||
(http.ResponseWriter, *http.Request) as parameters.
|
||||
|
||||
Paths can have variables. They are defined using the format {name} or
|
||||
{name:pattern}. If a regular expression pattern is not defined, the matched
|
||||
variable will be anything until the next slash. For example:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/products/{key}", ProductHandler)
|
||||
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
|
||||
|
||||
Groups can be used inside patterns, as long as they are non-capturing (?:re). For example:
|
||||
|
||||
r.HandleFunc("/articles/{category}/{sort:(?:asc|desc|new)}", ArticlesCategoryHandler)
|
||||
|
||||
The names are used to create a map of route variables which can be retrieved
|
||||
calling mux.Vars():
|
||||
|
||||
vars := mux.Vars(request)
|
||||
category := vars["category"]
|
||||
|
||||
Note that if any capturing groups are present, mux will panic() during parsing. To prevent
|
||||
this, convert any capturing groups to non-capturing, e.g. change "/{sort:(asc|desc)}" to
|
||||
"/{sort:(?:asc|desc)}". This is a change from prior versions which behaved unpredictably
|
||||
when capturing groups were present.
|
||||
|
||||
And this is all you need to know about the basic usage. More advanced options
|
||||
are explained below.
|
||||
|
||||
Routes can also be restricted to a domain or subdomain. Just define a host
|
||||
pattern to be matched. They can also have variables:
|
||||
|
||||
r := mux.NewRouter()
|
||||
// Only matches if domain is "www.example.com".
|
||||
r.Host("www.example.com")
|
||||
// Matches a dynamic subdomain.
|
||||
r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
|
||||
There are several other matchers that can be added. To match path prefixes:
|
||||
|
||||
r.PathPrefix("/products/")
|
||||
|
||||
...or HTTP methods:
|
||||
|
||||
r.Methods("GET", "POST")
|
||||
|
||||
...or URL schemes:
|
||||
|
||||
r.Schemes("https")
|
||||
|
||||
...or header values:
|
||||
|
||||
r.Headers("X-Requested-With", "XMLHttpRequest")
|
||||
|
||||
...or query values:
|
||||
|
||||
r.Queries("key", "value")
|
||||
|
||||
...or to use a custom matcher function:
|
||||
|
||||
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
|
||||
return r.ProtoMajor == 0
|
||||
})
|
||||
|
||||
...and finally, it is possible to combine several matchers in a single route:
|
||||
|
||||
r.HandleFunc("/products", ProductsHandler).
|
||||
Host("www.example.com").
|
||||
Methods("GET").
|
||||
Schemes("http")
|
||||
|
||||
Setting the same matching conditions again and again can be boring, so we have
|
||||
a way to group several routes that share the same requirements.
|
||||
We call it "subrouting".
|
||||
|
||||
For example, let's say we have several URLs that should only match when the
|
||||
host is "www.example.com". Create a route for that host and get a "subrouter"
|
||||
from it:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("www.example.com").Subrouter()
|
||||
|
||||
Then register routes in the subrouter:
|
||||
|
||||
s.HandleFunc("/products/", ProductsHandler)
|
||||
s.HandleFunc("/products/{key}", ProductHandler)
|
||||
s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
|
||||
The three URL paths we registered above will only be tested if the domain is
|
||||
"www.example.com", because the subrouter is tested first. This is not
|
||||
only convenient, but also optimizes request matching. You can create
|
||||
subrouters combining any attribute matchers accepted by a route.
|
||||
|
||||
Subrouters can be used to create domain or path "namespaces": you define
|
||||
subrouters in a central place and then parts of the app can register its
|
||||
paths relatively to a given subrouter.
|
||||
|
||||
There's one more thing about subroutes. When a subrouter has a path prefix,
|
||||
the inner routes use it as base for their paths:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.PathPrefix("/products").Subrouter()
|
||||
// "/products/"
|
||||
s.HandleFunc("/", ProductsHandler)
|
||||
// "/products/{key}/"
|
||||
s.HandleFunc("/{key}/", ProductHandler)
|
||||
// "/products/{key}/details"
|
||||
s.HandleFunc("/{key}/details", ProductDetailsHandler)
|
||||
|
||||
Note that the path provided to PathPrefix() represents a "wildcard": calling
|
||||
PathPrefix("/static/").Handler(...) means that the handler will be passed any
|
||||
request that matches "/static/*". This makes it easy to serve static files with mux:
|
||||
|
||||
func main() {
|
||||
var dir string
|
||||
|
||||
flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir")
|
||||
flag.Parse()
|
||||
r := mux.NewRouter()
|
||||
|
||||
// This will serve files under http://localhost:8000/static/<filename>
|
||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir))))
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: r,
|
||||
Addr: "127.0.0.1:8000",
|
||||
// Good practice: enforce timeouts for servers you create!
|
||||
WriteTimeout: 15 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
|
||||
Now let's see how to build registered URLs.
|
||||
|
||||
Routes can be named. All routes that define a name can have their URLs built,
|
||||
or "reversed". We define a name calling Name() on a route. For example:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
To build a URL, get the route and call the URL() method, passing a sequence of
|
||||
key/value pairs for the route variables. For the previous route, we would do:
|
||||
|
||||
url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
|
||||
...and the result will be a url.URL with the following path:
|
||||
|
||||
"/articles/technology/42"
|
||||
|
||||
This also works for host and query value variables:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.Host("{subdomain}.domain.com").
|
||||
Path("/articles/{category}/{id:[0-9]+}").
|
||||
Queries("filter", "{filter}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42",
|
||||
"filter", "gorilla")
|
||||
|
||||
All variables defined in the route are required, and their values must
|
||||
conform to the corresponding patterns. These requirements guarantee that a
|
||||
generated URL will always match a registered route -- the only exception is
|
||||
for explicitly defined "build-only" routes which never match.
|
||||
|
||||
Regex support also exists for matching Headers within a route. For example, we could do:
|
||||
|
||||
r.HeadersRegexp("Content-Type", "application/(text|json)")
|
||||
|
||||
...and the route will match both requests with a Content-Type of `application/json` as well as
|
||||
`application/text`
|
||||
|
||||
There's also a way to build only the URL host or path for a route:
|
||||
use the methods URLHost() or URLPath() instead. For the previous route,
|
||||
we would do:
|
||||
|
||||
// "http://news.domain.com/"
|
||||
host, err := r.Get("article").URLHost("subdomain", "news")
|
||||
|
||||
// "/articles/technology/42"
|
||||
path, err := r.Get("article").URLPath("category", "technology", "id", "42")
|
||||
|
||||
And if you use subrouters, host and path defined separately can be built
|
||||
as well:
|
||||
|
||||
r := mux.NewRouter()
|
||||
s := r.Host("{subdomain}.domain.com").Subrouter()
|
||||
s.Path("/articles/{category}/{id:[0-9]+}").
|
||||
HandlerFunc(ArticleHandler).
|
||||
Name("article")
|
||||
|
||||
// "http://news.domain.com/articles/technology/42"
|
||||
url, err := r.Get("article").URL("subdomain", "news",
|
||||
"category", "technology",
|
||||
"id", "42")
|
||||
|
||||
Mux supports the addition of middlewares to a Router, which are executed in the order they are added if a match is found, including its subrouters. Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or ResponseWriter hijacking.
|
||||
|
||||
type MiddlewareFunc func(http.Handler) http.Handler
|
||||
|
||||
Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created).
|
||||
|
||||
A very basic middleware which logs the URI of the request being handled could be written as:
|
||||
|
||||
func simpleMw(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Do stuff here
|
||||
log.Println(r.RequestURI)
|
||||
// Call the next handler, which can be another middleware in the chain, or the final handler.
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
Middlewares can be added to a router using `Router.Use()`:
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", handler)
|
||||
r.Use(simpleMw)
|
||||
|
||||
A more complex authentication middleware, which maps session token to users, could be written as:
|
||||
|
||||
// Define our struct
|
||||
type authenticationMiddleware struct {
|
||||
tokenUsers map[string]string
|
||||
}
|
||||
|
||||
// Initialize it somewhere
|
||||
func (amw *authenticationMiddleware) Populate() {
|
||||
amw.tokenUsers["00000000"] = "user0"
|
||||
amw.tokenUsers["aaaaaaaa"] = "userA"
|
||||
amw.tokenUsers["05f717e5"] = "randomUser"
|
||||
amw.tokenUsers["deadbeef"] = "user0"
|
||||
}
|
||||
|
||||
// Middleware function, which will be called for each request
|
||||
func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("X-Session-Token")
|
||||
|
||||
if user, found := amw.tokenUsers[token]; found {
|
||||
// We found the token in our map
|
||||
log.Printf("Authenticated user %s\n", user)
|
||||
next.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", handler)
|
||||
|
||||
amw := authenticationMiddleware{tokenUsers: make(map[string]string)}
|
||||
amw.Populate()
|
||||
|
||||
r.Use(amw.Middleware)
|
||||
|
||||
Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to.
|
||||
*/
|
||||
package mux
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler.
|
||||
// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed
|
||||
// to it, and then calls the handler passed as parameter to the MiddlewareFunc.
|
||||
type MiddlewareFunc func(http.Handler) http.Handler
|
||||
|
||||
// middleware interface is anything which implements a MiddlewareFunc named Middleware.
|
||||
type middleware interface {
|
||||
Middleware(handler http.Handler) http.Handler
|
||||
}
|
||||
|
||||
// Middleware allows MiddlewareFunc to implement the middleware interface.
|
||||
func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler {
|
||||
return mw(handler)
|
||||
}
|
||||
|
||||
// Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router.
|
||||
func (r *Router) Use(mwf ...MiddlewareFunc) {
|
||||
for _, fn := range mwf {
|
||||
r.middlewares = append(r.middlewares, fn)
|
||||
}
|
||||
}
|
||||
|
||||
// useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router.
|
||||
func (r *Router) useInterface(mw middleware) {
|
||||
r.middlewares = append(r.middlewares, mw)
|
||||
}
|
||||
|
||||
// CORSMethodMiddleware automatically sets the Access-Control-Allow-Methods response header
|
||||
// on requests for routes that have an OPTIONS method matcher to all the method matchers on
|
||||
// the route. Routes that do not explicitly handle OPTIONS requests will not be processed
|
||||
// by the middleware. See examples for usage.
|
||||
func CORSMethodMiddleware(r *Router) MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
allMethods, err := getAllMethodsForRoute(r, req)
|
||||
if err == nil {
|
||||
for _, v := range allMethods {
|
||||
if v == http.MethodOptions {
|
||||
w.Header().Set("Access-Control-Allow-Methods", strings.Join(allMethods, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// getAllMethodsForRoute returns all the methods from method matchers matching a given
|
||||
// request.
|
||||
func getAllMethodsForRoute(r *Router, req *http.Request) ([]string, error) {
|
||||
var allMethods []string
|
||||
|
||||
for _, route := range r.routes {
|
||||
var match RouteMatch
|
||||
if route.Match(req, &match) || match.MatchErr == ErrMethodMismatch {
|
||||
methods, err := route.GetMethods()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allMethods = append(allMethods, methods...)
|
||||
}
|
||||
}
|
||||
|
||||
return allMethods, nil
|
||||
}
|
||||
+608
@@ -0,0 +1,608 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMethodMismatch is returned when the method in the request does not match
|
||||
// the method defined against the route.
|
||||
ErrMethodMismatch = errors.New("method is not allowed")
|
||||
// ErrNotFound is returned when no route match is found.
|
||||
ErrNotFound = errors.New("no matching route was found")
|
||||
)
|
||||
|
||||
// NewRouter returns a new router instance.
|
||||
func NewRouter() *Router {
|
||||
return &Router{namedRoutes: make(map[string]*Route)}
|
||||
}
|
||||
|
||||
// Router registers routes to be matched and dispatches a handler.
|
||||
//
|
||||
// It implements the http.Handler interface, so it can be registered to serve
|
||||
// requests:
|
||||
//
|
||||
// var router = mux.NewRouter()
|
||||
//
|
||||
// func main() {
|
||||
// http.Handle("/", router)
|
||||
// }
|
||||
//
|
||||
// Or, for Google App Engine, register it in a init() function:
|
||||
//
|
||||
// func init() {
|
||||
// http.Handle("/", router)
|
||||
// }
|
||||
//
|
||||
// This will send all incoming requests to the router.
|
||||
type Router struct {
|
||||
// Configurable Handler to be used when no route matches.
|
||||
// This can be used to render your own 404 Not Found errors.
|
||||
NotFoundHandler http.Handler
|
||||
|
||||
// Configurable Handler to be used when the request method does not match the route.
|
||||
// This can be used to render your own 405 Method Not Allowed errors.
|
||||
MethodNotAllowedHandler http.Handler
|
||||
|
||||
// Routes to be matched, in order.
|
||||
routes []*Route
|
||||
|
||||
// Routes by name for URL building.
|
||||
namedRoutes map[string]*Route
|
||||
|
||||
// If true, do not clear the request context after handling the request.
|
||||
//
|
||||
// Deprecated: No effect, since the context is stored on the request itself.
|
||||
KeepContext bool
|
||||
|
||||
// Slice of middlewares to be called after a match is found
|
||||
middlewares []middleware
|
||||
|
||||
// configuration shared with `Route`
|
||||
routeConf
|
||||
}
|
||||
|
||||
// common route configuration shared between `Router` and `Route`
|
||||
type routeConf struct {
|
||||
// If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to"
|
||||
useEncodedPath bool
|
||||
|
||||
// If true, when the path pattern is "/path/", accessing "/path" will
|
||||
// redirect to the former and vice versa.
|
||||
strictSlash bool
|
||||
|
||||
// If true, when the path pattern is "/path//to", accessing "/path//to"
|
||||
// will not redirect
|
||||
skipClean bool
|
||||
|
||||
// Manager for the variables from host and path.
|
||||
regexp routeRegexpGroup
|
||||
|
||||
// List of matchers.
|
||||
matchers []matcher
|
||||
|
||||
// The scheme used when building URLs.
|
||||
buildScheme string
|
||||
|
||||
buildVarsFunc BuildVarsFunc
|
||||
}
|
||||
|
||||
// returns an effective deep copy of `routeConf`
|
||||
func copyRouteConf(r routeConf) routeConf {
|
||||
c := r
|
||||
|
||||
if r.regexp.path != nil {
|
||||
c.regexp.path = copyRouteRegexp(r.regexp.path)
|
||||
}
|
||||
|
||||
if r.regexp.host != nil {
|
||||
c.regexp.host = copyRouteRegexp(r.regexp.host)
|
||||
}
|
||||
|
||||
c.regexp.queries = make([]*routeRegexp, 0, len(r.regexp.queries))
|
||||
for _, q := range r.regexp.queries {
|
||||
c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q))
|
||||
}
|
||||
|
||||
c.matchers = make([]matcher, len(r.matchers))
|
||||
copy(c.matchers, r.matchers)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func copyRouteRegexp(r *routeRegexp) *routeRegexp {
|
||||
c := *r
|
||||
return &c
|
||||
}
|
||||
|
||||
// Match attempts to match the given request against the router's registered routes.
|
||||
//
|
||||
// If the request matches a route of this router or one of its subrouters the Route,
|
||||
// Handler, and Vars fields of the the match argument are filled and this function
|
||||
// returns true.
|
||||
//
|
||||
// If the request does not match any of this router's or its subrouters' routes
|
||||
// then this function returns false. If available, a reason for the match failure
|
||||
// will be filled in the match argument's MatchErr field. If the match failure type
|
||||
// (eg: not found) has a registered handler, the handler is assigned to the Handler
|
||||
// field of the match argument.
|
||||
func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
|
||||
for _, route := range r.routes {
|
||||
if route.Match(req, match) {
|
||||
// Build middleware chain if no error was found
|
||||
if match.MatchErr == nil {
|
||||
for i := len(r.middlewares) - 1; i >= 0; i-- {
|
||||
match.Handler = r.middlewares[i].Middleware(match.Handler)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if match.MatchErr == ErrMethodMismatch {
|
||||
if r.MethodNotAllowedHandler != nil {
|
||||
match.Handler = r.MethodNotAllowedHandler
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Closest match for a router (includes sub-routers)
|
||||
if r.NotFoundHandler != nil {
|
||||
match.Handler = r.NotFoundHandler
|
||||
match.MatchErr = ErrNotFound
|
||||
return true
|
||||
}
|
||||
|
||||
match.MatchErr = ErrNotFound
|
||||
return false
|
||||
}
|
||||
|
||||
// ServeHTTP dispatches the handler registered in the matched route.
|
||||
//
|
||||
// When there is a match, the route variables can be retrieved calling
|
||||
// mux.Vars(request).
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
if !r.skipClean {
|
||||
path := req.URL.Path
|
||||
if r.useEncodedPath {
|
||||
path = req.URL.EscapedPath()
|
||||
}
|
||||
// Clean path to canonical form and redirect.
|
||||
if p := cleanPath(path); p != path {
|
||||
|
||||
// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
|
||||
// This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue:
|
||||
// http://code.google.com/p/go/issues/detail?id=5252
|
||||
url := *req.URL
|
||||
url.Path = p
|
||||
p = url.String()
|
||||
|
||||
w.Header().Set("Location", p)
|
||||
w.WriteHeader(http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
}
|
||||
var match RouteMatch
|
||||
var handler http.Handler
|
||||
if r.Match(req, &match) {
|
||||
handler = match.Handler
|
||||
req = requestWithVars(req, match.Vars)
|
||||
req = requestWithRoute(req, match.Route)
|
||||
}
|
||||
|
||||
if handler == nil && match.MatchErr == ErrMethodMismatch {
|
||||
handler = methodNotAllowedHandler()
|
||||
}
|
||||
|
||||
if handler == nil {
|
||||
handler = http.NotFoundHandler()
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// Get returns a route registered with the given name.
|
||||
func (r *Router) Get(name string) *Route {
|
||||
return r.namedRoutes[name]
|
||||
}
|
||||
|
||||
// GetRoute returns a route registered with the given name. This method
|
||||
// was renamed to Get() and remains here for backwards compatibility.
|
||||
func (r *Router) GetRoute(name string) *Route {
|
||||
return r.namedRoutes[name]
|
||||
}
|
||||
|
||||
// StrictSlash defines the trailing slash behavior for new routes. The initial
|
||||
// value is false.
|
||||
//
|
||||
// When true, if the route path is "/path/", accessing "/path" will perform a redirect
|
||||
// to the former and vice versa. In other words, your application will always
|
||||
// see the path as specified in the route.
|
||||
//
|
||||
// When false, if the route path is "/path", accessing "/path/" will not match
|
||||
// this route and vice versa.
|
||||
//
|
||||
// The re-direct is a HTTP 301 (Moved Permanently). Note that when this is set for
|
||||
// routes with a non-idempotent method (e.g. POST, PUT), the subsequent re-directed
|
||||
// request will be made as a GET by most clients. Use middleware or client settings
|
||||
// to modify this behaviour as needed.
|
||||
//
|
||||
// Special case: when a route sets a path prefix using the PathPrefix() method,
|
||||
// strict slash is ignored for that route because the redirect behavior can't
|
||||
// be determined from a prefix alone. However, any subrouters created from that
|
||||
// route inherit the original StrictSlash setting.
|
||||
func (r *Router) StrictSlash(value bool) *Router {
|
||||
r.strictSlash = value
|
||||
return r
|
||||
}
|
||||
|
||||
// SkipClean defines the path cleaning behaviour for new routes. The initial
|
||||
// value is false. Users should be careful about which routes are not cleaned
|
||||
//
|
||||
// When true, if the route path is "/path//to", it will remain with the double
|
||||
// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/
|
||||
//
|
||||
// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will
|
||||
// become /fetch/http/xkcd.com/534
|
||||
func (r *Router) SkipClean(value bool) *Router {
|
||||
r.skipClean = value
|
||||
return r
|
||||
}
|
||||
|
||||
// UseEncodedPath tells the router to match the encoded original path
|
||||
// to the routes.
|
||||
// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to".
|
||||
//
|
||||
// If not called, the router will match the unencoded path to the routes.
|
||||
// For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to"
|
||||
func (r *Router) UseEncodedPath() *Router {
|
||||
r.useEncodedPath = true
|
||||
return r
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Route factories
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// NewRoute registers an empty route.
|
||||
func (r *Router) NewRoute() *Route {
|
||||
// initialize a route with a copy of the parent router's configuration
|
||||
route := &Route{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes}
|
||||
r.routes = append(r.routes, route)
|
||||
return route
|
||||
}
|
||||
|
||||
// Name registers a new route with a name.
|
||||
// See Route.Name().
|
||||
func (r *Router) Name(name string) *Route {
|
||||
return r.NewRoute().Name(name)
|
||||
}
|
||||
|
||||
// Handle registers a new route with a matcher for the URL path.
|
||||
// See Route.Path() and Route.Handler().
|
||||
func (r *Router) Handle(path string, handler http.Handler) *Route {
|
||||
return r.NewRoute().Path(path).Handler(handler)
|
||||
}
|
||||
|
||||
// HandleFunc registers a new route with a matcher for the URL path.
|
||||
// See Route.Path() and Route.HandlerFunc().
|
||||
func (r *Router) HandleFunc(path string, f func(http.ResponseWriter,
|
||||
*http.Request)) *Route {
|
||||
return r.NewRoute().Path(path).HandlerFunc(f)
|
||||
}
|
||||
|
||||
// Headers registers a new route with a matcher for request header values.
|
||||
// See Route.Headers().
|
||||
func (r *Router) Headers(pairs ...string) *Route {
|
||||
return r.NewRoute().Headers(pairs...)
|
||||
}
|
||||
|
||||
// Host registers a new route with a matcher for the URL host.
|
||||
// See Route.Host().
|
||||
func (r *Router) Host(tpl string) *Route {
|
||||
return r.NewRoute().Host(tpl)
|
||||
}
|
||||
|
||||
// MatcherFunc registers a new route with a custom matcher function.
|
||||
// See Route.MatcherFunc().
|
||||
func (r *Router) MatcherFunc(f MatcherFunc) *Route {
|
||||
return r.NewRoute().MatcherFunc(f)
|
||||
}
|
||||
|
||||
// Methods registers a new route with a matcher for HTTP methods.
|
||||
// See Route.Methods().
|
||||
func (r *Router) Methods(methods ...string) *Route {
|
||||
return r.NewRoute().Methods(methods...)
|
||||
}
|
||||
|
||||
// Path registers a new route with a matcher for the URL path.
|
||||
// See Route.Path().
|
||||
func (r *Router) Path(tpl string) *Route {
|
||||
return r.NewRoute().Path(tpl)
|
||||
}
|
||||
|
||||
// PathPrefix registers a new route with a matcher for the URL path prefix.
|
||||
// See Route.PathPrefix().
|
||||
func (r *Router) PathPrefix(tpl string) *Route {
|
||||
return r.NewRoute().PathPrefix(tpl)
|
||||
}
|
||||
|
||||
// Queries registers a new route with a matcher for URL query values.
|
||||
// See Route.Queries().
|
||||
func (r *Router) Queries(pairs ...string) *Route {
|
||||
return r.NewRoute().Queries(pairs...)
|
||||
}
|
||||
|
||||
// Schemes registers a new route with a matcher for URL schemes.
|
||||
// See Route.Schemes().
|
||||
func (r *Router) Schemes(schemes ...string) *Route {
|
||||
return r.NewRoute().Schemes(schemes...)
|
||||
}
|
||||
|
||||
// BuildVarsFunc registers a new route with a custom function for modifying
|
||||
// route variables before building a URL.
|
||||
func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route {
|
||||
return r.NewRoute().BuildVarsFunc(f)
|
||||
}
|
||||
|
||||
// Walk walks the router and all its sub-routers, calling walkFn for each route
|
||||
// in the tree. The routes are walked in the order they were added. Sub-routers
|
||||
// are explored depth-first.
|
||||
func (r *Router) Walk(walkFn WalkFunc) error {
|
||||
return r.walk(walkFn, []*Route{})
|
||||
}
|
||||
|
||||
// SkipRouter is used as a return value from WalkFuncs to indicate that the
|
||||
// router that walk is about to descend down to should be skipped.
|
||||
var SkipRouter = errors.New("skip this router")
|
||||
|
||||
// WalkFunc is the type of the function called for each route visited by Walk.
|
||||
// At every invocation, it is given the current route, and the current router,
|
||||
// and a list of ancestor routes that lead to the current route.
|
||||
type WalkFunc func(route *Route, router *Router, ancestors []*Route) error
|
||||
|
||||
func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error {
|
||||
for _, t := range r.routes {
|
||||
err := walkFn(t, r, ancestors)
|
||||
if err == SkipRouter {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, sr := range t.matchers {
|
||||
if h, ok := sr.(*Router); ok {
|
||||
ancestors = append(ancestors, t)
|
||||
err := h.walk(walkFn, ancestors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ancestors = ancestors[:len(ancestors)-1]
|
||||
}
|
||||
}
|
||||
if h, ok := t.handler.(*Router); ok {
|
||||
ancestors = append(ancestors, t)
|
||||
err := h.walk(walkFn, ancestors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ancestors = ancestors[:len(ancestors)-1]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Context
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// RouteMatch stores information about a matched route.
|
||||
type RouteMatch struct {
|
||||
Route *Route
|
||||
Handler http.Handler
|
||||
Vars map[string]string
|
||||
|
||||
// MatchErr is set to appropriate matching error
|
||||
// It is set to ErrMethodMismatch if there is a mismatch in
|
||||
// the request method and route method
|
||||
MatchErr error
|
||||
}
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
varsKey contextKey = iota
|
||||
routeKey
|
||||
)
|
||||
|
||||
// Vars returns the route variables for the current request, if any.
|
||||
func Vars(r *http.Request) map[string]string {
|
||||
if rv := r.Context().Value(varsKey); rv != nil {
|
||||
return rv.(map[string]string)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CurrentRoute returns the matched route for the current request, if any.
|
||||
// This only works when called inside the handler of the matched route
|
||||
// because the matched route is stored in the request context which is cleared
|
||||
// after the handler returns.
|
||||
func CurrentRoute(r *http.Request) *Route {
|
||||
if rv := r.Context().Value(routeKey); rv != nil {
|
||||
return rv.(*Route)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func requestWithVars(r *http.Request, vars map[string]string) *http.Request {
|
||||
ctx := context.WithValue(r.Context(), varsKey, vars)
|
||||
return r.WithContext(ctx)
|
||||
}
|
||||
|
||||
func requestWithRoute(r *http.Request, route *Route) *http.Request {
|
||||
ctx := context.WithValue(r.Context(), routeKey, route)
|
||||
return r.WithContext(ctx)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// cleanPath returns the canonical path for p, eliminating . and .. elements.
|
||||
// Borrowed from the net/http package.
|
||||
func cleanPath(p string) string {
|
||||
if p == "" {
|
||||
return "/"
|
||||
}
|
||||
if p[0] != '/' {
|
||||
p = "/" + p
|
||||
}
|
||||
np := path.Clean(p)
|
||||
// path.Clean removes trailing slash except for root;
|
||||
// put the trailing slash back if necessary.
|
||||
if p[len(p)-1] == '/' && np != "/" {
|
||||
np += "/"
|
||||
}
|
||||
|
||||
return np
|
||||
}
|
||||
|
||||
// uniqueVars returns an error if two slices contain duplicated strings.
|
||||
func uniqueVars(s1, s2 []string) error {
|
||||
for _, v1 := range s1 {
|
||||
for _, v2 := range s2 {
|
||||
if v1 == v2 {
|
||||
return fmt.Errorf("mux: duplicated route variable %q", v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkPairs returns the count of strings passed in, and an error if
|
||||
// the count is not an even number.
|
||||
func checkPairs(pairs ...string) (int, error) {
|
||||
length := len(pairs)
|
||||
if length%2 != 0 {
|
||||
return length, fmt.Errorf(
|
||||
"mux: number of parameters must be multiple of 2, got %v", pairs)
|
||||
}
|
||||
return length, nil
|
||||
}
|
||||
|
||||
// mapFromPairsToString converts variadic string parameters to a
|
||||
// string to string map.
|
||||
func mapFromPairsToString(pairs ...string) (map[string]string, error) {
|
||||
length, err := checkPairs(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]string, length/2)
|
||||
for i := 0; i < length; i += 2 {
|
||||
m[pairs[i]] = pairs[i+1]
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// mapFromPairsToRegex converts variadic string parameters to a
|
||||
// string to regex map.
|
||||
func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) {
|
||||
length, err := checkPairs(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*regexp.Regexp, length/2)
|
||||
for i := 0; i < length; i += 2 {
|
||||
regex, err := regexp.Compile(pairs[i+1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[pairs[i]] = regex
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// matchInArray returns true if the given string value is in the array.
|
||||
func matchInArray(arr []string, value string) bool {
|
||||
for _, v := range arr {
|
||||
if v == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchMapWithString returns true if the given key/value pairs exist in a given map.
|
||||
func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool {
|
||||
for k, v := range toCheck {
|
||||
// Check if key exists.
|
||||
if canonicalKey {
|
||||
k = http.CanonicalHeaderKey(k)
|
||||
}
|
||||
if values := toMatch[k]; values == nil {
|
||||
return false
|
||||
} else if v != "" {
|
||||
// If value was defined as an empty string we only check that the
|
||||
// key exists. Otherwise we also check for equality.
|
||||
valueExists := false
|
||||
for _, value := range values {
|
||||
if v == value {
|
||||
valueExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valueExists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against
|
||||
// the given regex
|
||||
func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool {
|
||||
for k, v := range toCheck {
|
||||
// Check if key exists.
|
||||
if canonicalKey {
|
||||
k = http.CanonicalHeaderKey(k)
|
||||
}
|
||||
if values := toMatch[k]; values == nil {
|
||||
return false
|
||||
} else if v != nil {
|
||||
// If value was defined as an empty string we only check that the
|
||||
// key exists. Otherwise we also check for equality.
|
||||
valueExists := false
|
||||
for _, value := range values {
|
||||
if v.MatchString(value) {
|
||||
valueExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valueExists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// methodNotAllowed replies to the request with an HTTP status code 405.
|
||||
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
// methodNotAllowedHandler returns a simple request handler
|
||||
// that replies to each request with a status code 405.
|
||||
func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) }
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type routeRegexpOptions struct {
|
||||
strictSlash bool
|
||||
useEncodedPath bool
|
||||
}
|
||||
|
||||
type regexpType int
|
||||
|
||||
const (
|
||||
regexpTypePath regexpType = iota
|
||||
regexpTypeHost
|
||||
regexpTypePrefix
|
||||
regexpTypeQuery
|
||||
)
|
||||
|
||||
// newRouteRegexp parses a route template and returns a routeRegexp,
|
||||
// used to match a host, a path or a query string.
|
||||
//
|
||||
// It will extract named variables, assemble a regexp to be matched, create
|
||||
// a "reverse" template to build URLs and compile regexps to validate variable
|
||||
// values used in URL building.
|
||||
//
|
||||
// Previously we accepted only Python-like identifiers for variable
|
||||
// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
|
||||
// name and pattern can't be empty, and names can't contain a colon.
|
||||
func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) {
|
||||
// Check if it is well-formed.
|
||||
idxs, errBraces := braceIndices(tpl)
|
||||
if errBraces != nil {
|
||||
return nil, errBraces
|
||||
}
|
||||
// Backup the original.
|
||||
template := tpl
|
||||
// Now let's parse it.
|
||||
defaultPattern := "[^/]+"
|
||||
if typ == regexpTypeQuery {
|
||||
defaultPattern = ".*"
|
||||
} else if typ == regexpTypeHost {
|
||||
defaultPattern = "[^.]+"
|
||||
}
|
||||
// Only match strict slash if not matching
|
||||
if typ != regexpTypePath {
|
||||
options.strictSlash = false
|
||||
}
|
||||
// Set a flag for strictSlash.
|
||||
endSlash := false
|
||||
if options.strictSlash && strings.HasSuffix(tpl, "/") {
|
||||
tpl = tpl[:len(tpl)-1]
|
||||
endSlash = true
|
||||
}
|
||||
varsN := make([]string, len(idxs)/2)
|
||||
varsR := make([]*regexp.Regexp, len(idxs)/2)
|
||||
pattern := bytes.NewBufferString("")
|
||||
pattern.WriteByte('^')
|
||||
reverse := bytes.NewBufferString("")
|
||||
var end int
|
||||
var err error
|
||||
for i := 0; i < len(idxs); i += 2 {
|
||||
// Set all values we are interested in.
|
||||
raw := tpl[end:idxs[i]]
|
||||
end = idxs[i+1]
|
||||
parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2)
|
||||
name := parts[0]
|
||||
patt := defaultPattern
|
||||
if len(parts) == 2 {
|
||||
patt = parts[1]
|
||||
}
|
||||
// Name or pattern can't be empty.
|
||||
if name == "" || patt == "" {
|
||||
return nil, fmt.Errorf("mux: missing name or pattern in %q",
|
||||
tpl[idxs[i]:end])
|
||||
}
|
||||
// Build the regexp pattern.
|
||||
fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt)
|
||||
|
||||
// Build the reverse template.
|
||||
fmt.Fprintf(reverse, "%s%%s", raw)
|
||||
|
||||
// Append variable name and compiled pattern.
|
||||
varsN[i/2] = name
|
||||
varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Add the remaining.
|
||||
raw := tpl[end:]
|
||||
pattern.WriteString(regexp.QuoteMeta(raw))
|
||||
if options.strictSlash {
|
||||
pattern.WriteString("[/]?")
|
||||
}
|
||||
if typ == regexpTypeQuery {
|
||||
// Add the default pattern if the query value is empty
|
||||
if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" {
|
||||
pattern.WriteString(defaultPattern)
|
||||
}
|
||||
}
|
||||
if typ != regexpTypePrefix {
|
||||
pattern.WriteByte('$')
|
||||
}
|
||||
|
||||
var wildcardHostPort bool
|
||||
if typ == regexpTypeHost {
|
||||
if !strings.Contains(pattern.String(), ":") {
|
||||
wildcardHostPort = true
|
||||
}
|
||||
}
|
||||
reverse.WriteString(raw)
|
||||
if endSlash {
|
||||
reverse.WriteByte('/')
|
||||
}
|
||||
// Compile full regexp.
|
||||
reg, errCompile := regexp.Compile(pattern.String())
|
||||
if errCompile != nil {
|
||||
return nil, errCompile
|
||||
}
|
||||
|
||||
// Check for capturing groups which used to work in older versions
|
||||
if reg.NumSubexp() != len(idxs)/2 {
|
||||
panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) +
|
||||
"Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)")
|
||||
}
|
||||
|
||||
// Done!
|
||||
return &routeRegexp{
|
||||
template: template,
|
||||
regexpType: typ,
|
||||
options: options,
|
||||
regexp: reg,
|
||||
reverse: reverse.String(),
|
||||
varsN: varsN,
|
||||
varsR: varsR,
|
||||
wildcardHostPort: wildcardHostPort,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// routeRegexp stores a regexp to match a host or path and information to
|
||||
// collect and validate route variables.
|
||||
type routeRegexp struct {
|
||||
// The unmodified template.
|
||||
template string
|
||||
// The type of match
|
||||
regexpType regexpType
|
||||
// Options for matching
|
||||
options routeRegexpOptions
|
||||
// Expanded regexp.
|
||||
regexp *regexp.Regexp
|
||||
// Reverse template.
|
||||
reverse string
|
||||
// Variable names.
|
||||
varsN []string
|
||||
// Variable regexps (validators).
|
||||
varsR []*regexp.Regexp
|
||||
// Wildcard host-port (no strict port match in hostname)
|
||||
wildcardHostPort bool
|
||||
}
|
||||
|
||||
// Match matches the regexp against the URL host or path.
|
||||
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
|
||||
if r.regexpType == regexpTypeHost {
|
||||
host := getHost(req)
|
||||
if r.wildcardHostPort {
|
||||
// Don't be strict on the port match
|
||||
if i := strings.Index(host, ":"); i != -1 {
|
||||
host = host[:i]
|
||||
}
|
||||
}
|
||||
return r.regexp.MatchString(host)
|
||||
}
|
||||
|
||||
if r.regexpType == regexpTypeQuery {
|
||||
return r.matchQueryString(req)
|
||||
}
|
||||
path := req.URL.Path
|
||||
if r.options.useEncodedPath {
|
||||
path = req.URL.EscapedPath()
|
||||
}
|
||||
return r.regexp.MatchString(path)
|
||||
}
|
||||
|
||||
// url builds a URL part using the given values.
|
||||
func (r *routeRegexp) url(values map[string]string) (string, error) {
|
||||
urlValues := make([]interface{}, len(r.varsN))
|
||||
for k, v := range r.varsN {
|
||||
value, ok := values[v]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("mux: missing route variable %q", v)
|
||||
}
|
||||
if r.regexpType == regexpTypeQuery {
|
||||
value = url.QueryEscape(value)
|
||||
}
|
||||
urlValues[k] = value
|
||||
}
|
||||
rv := fmt.Sprintf(r.reverse, urlValues...)
|
||||
if !r.regexp.MatchString(rv) {
|
||||
// The URL is checked against the full regexp, instead of checking
|
||||
// individual variables. This is faster but to provide a good error
|
||||
// message, we check individual regexps if the URL doesn't match.
|
||||
for k, v := range r.varsN {
|
||||
if !r.varsR[k].MatchString(values[v]) {
|
||||
return "", fmt.Errorf(
|
||||
"mux: variable %q doesn't match, expected %q", values[v],
|
||||
r.varsR[k].String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
// getURLQuery returns a single query parameter from a request URL.
|
||||
// For a URL with foo=bar&baz=ding, we return only the relevant key
|
||||
// value pair for the routeRegexp.
|
||||
func (r *routeRegexp) getURLQuery(req *http.Request) string {
|
||||
if r.regexpType != regexpTypeQuery {
|
||||
return ""
|
||||
}
|
||||
templateKey := strings.SplitN(r.template, "=", 2)[0]
|
||||
val, ok := findFirstQueryKey(req.URL.RawQuery, templateKey)
|
||||
if ok {
|
||||
return templateKey + "=" + val
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// findFirstQueryKey returns the same result as (*url.URL).Query()[key][0].
|
||||
// If key was not found, empty string and false is returned.
|
||||
func findFirstQueryKey(rawQuery, key string) (value string, ok bool) {
|
||||
query := []byte(rawQuery)
|
||||
for len(query) > 0 {
|
||||
foundKey := query
|
||||
if i := bytes.IndexAny(foundKey, "&;"); i >= 0 {
|
||||
foundKey, query = foundKey[:i], foundKey[i+1:]
|
||||
} else {
|
||||
query = query[:0]
|
||||
}
|
||||
if len(foundKey) == 0 {
|
||||
continue
|
||||
}
|
||||
var value []byte
|
||||
if i := bytes.IndexByte(foundKey, '='); i >= 0 {
|
||||
foundKey, value = foundKey[:i], foundKey[i+1:]
|
||||
}
|
||||
if len(foundKey) < len(key) {
|
||||
// Cannot possibly be key.
|
||||
continue
|
||||
}
|
||||
keyString, err := url.QueryUnescape(string(foundKey))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if keyString != key {
|
||||
continue
|
||||
}
|
||||
valueString, err := url.QueryUnescape(string(value))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return valueString, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (r *routeRegexp) matchQueryString(req *http.Request) bool {
|
||||
return r.regexp.MatchString(r.getURLQuery(req))
|
||||
}
|
||||
|
||||
// braceIndices returns the first level curly brace indices from a string.
|
||||
// It returns an error in case of unbalanced braces.
|
||||
func braceIndices(s string) ([]int, error) {
|
||||
var level, idx int
|
||||
var idxs []int
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '{':
|
||||
if level++; level == 1 {
|
||||
idx = i
|
||||
}
|
||||
case '}':
|
||||
if level--; level == 0 {
|
||||
idxs = append(idxs, idx, i+1)
|
||||
} else if level < 0 {
|
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if level != 0 {
|
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
|
||||
}
|
||||
return idxs, nil
|
||||
}
|
||||
|
||||
// varGroupName builds a capturing group name for the indexed variable.
|
||||
func varGroupName(idx int) string {
|
||||
return "v" + strconv.Itoa(idx)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// routeRegexpGroup
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// routeRegexpGroup groups the route matchers that carry variables.
|
||||
type routeRegexpGroup struct {
|
||||
host *routeRegexp
|
||||
path *routeRegexp
|
||||
queries []*routeRegexp
|
||||
}
|
||||
|
||||
// setMatch extracts the variables from the URL once a route matches.
|
||||
func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
|
||||
// Store host variables.
|
||||
if v.host != nil {
|
||||
host := getHost(req)
|
||||
if v.host.wildcardHostPort {
|
||||
// Don't be strict on the port match
|
||||
if i := strings.Index(host, ":"); i != -1 {
|
||||
host = host[:i]
|
||||
}
|
||||
}
|
||||
matches := v.host.regexp.FindStringSubmatchIndex(host)
|
||||
if len(matches) > 0 {
|
||||
extractVars(host, matches, v.host.varsN, m.Vars)
|
||||
}
|
||||
}
|
||||
path := req.URL.Path
|
||||
if r.useEncodedPath {
|
||||
path = req.URL.EscapedPath()
|
||||
}
|
||||
// Store path variables.
|
||||
if v.path != nil {
|
||||
matches := v.path.regexp.FindStringSubmatchIndex(path)
|
||||
if len(matches) > 0 {
|
||||
extractVars(path, matches, v.path.varsN, m.Vars)
|
||||
// Check if we should redirect.
|
||||
if v.path.options.strictSlash {
|
||||
p1 := strings.HasSuffix(path, "/")
|
||||
p2 := strings.HasSuffix(v.path.template, "/")
|
||||
if p1 != p2 {
|
||||
u, _ := url.Parse(req.URL.String())
|
||||
if p1 {
|
||||
u.Path = u.Path[:len(u.Path)-1]
|
||||
} else {
|
||||
u.Path += "/"
|
||||
}
|
||||
m.Handler = http.RedirectHandler(u.String(), http.StatusMovedPermanently)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Store query string variables.
|
||||
for _, q := range v.queries {
|
||||
queryURL := q.getURLQuery(req)
|
||||
matches := q.regexp.FindStringSubmatchIndex(queryURL)
|
||||
if len(matches) > 0 {
|
||||
extractVars(queryURL, matches, q.varsN, m.Vars)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getHost tries its best to return the request host.
|
||||
// According to section 14.23 of RFC 2616 the Host header
|
||||
// can include the port number if the default value of 80 is not used.
|
||||
func getHost(r *http.Request) string {
|
||||
if r.URL.IsAbs() {
|
||||
return r.URL.Host
|
||||
}
|
||||
return r.Host
|
||||
}
|
||||
|
||||
func extractVars(input string, matches []int, names []string, output map[string]string) {
|
||||
for i, name := range names {
|
||||
output[name] = input[matches[2*i+2]:matches[2*i+3]]
|
||||
}
|
||||
}
|
||||
+765
@@ -0,0 +1,765 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Route stores information to match a request and build URLs.
|
||||
type Route struct {
|
||||
// Request handler for the route.
|
||||
handler http.Handler
|
||||
// If true, this route never matches: it is only used to build URLs.
|
||||
buildOnly bool
|
||||
// The name used to build URLs.
|
||||
name string
|
||||
// Error resulted from building a route.
|
||||
err error
|
||||
|
||||
// "global" reference to all named routes
|
||||
namedRoutes map[string]*Route
|
||||
|
||||
// config possibly passed in from `Router`
|
||||
routeConf
|
||||
}
|
||||
|
||||
// SkipClean reports whether path cleaning is enabled for this route via
|
||||
// Router.SkipClean.
|
||||
func (r *Route) SkipClean() bool {
|
||||
return r.skipClean
|
||||
}
|
||||
|
||||
// Match matches the route against the request.
|
||||
func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
|
||||
if r.buildOnly || r.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var matchErr error
|
||||
|
||||
// Match everything.
|
||||
for _, m := range r.matchers {
|
||||
if matched := m.Match(req, match); !matched {
|
||||
if _, ok := m.(methodMatcher); ok {
|
||||
matchErr = ErrMethodMismatch
|
||||
continue
|
||||
}
|
||||
|
||||
// Ignore ErrNotFound errors. These errors arise from match call
|
||||
// to Subrouters.
|
||||
//
|
||||
// This prevents subsequent matching subrouters from failing to
|
||||
// run middleware. If not ignored, the middleware would see a
|
||||
// non-nil MatchErr and be skipped, even when there was a
|
||||
// matching route.
|
||||
if match.MatchErr == ErrNotFound {
|
||||
match.MatchErr = nil
|
||||
}
|
||||
|
||||
matchErr = nil // nolint:ineffassign
|
||||
return false
|
||||
} else {
|
||||
// Multiple routes may share the same path but use different HTTP methods. For instance:
|
||||
// Route 1: POST "/users/{id}".
|
||||
// Route 2: GET "/users/{id}", parameters: "id": "[0-9]+".
|
||||
//
|
||||
// The router must handle these cases correctly. For a GET request to "/users/abc" with "id" as "-2",
|
||||
// The router should return a "Not Found" error as no route fully matches this request.
|
||||
if match.MatchErr == ErrMethodMismatch {
|
||||
match.MatchErr = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matchErr != nil {
|
||||
match.MatchErr = matchErr
|
||||
return false
|
||||
}
|
||||
|
||||
if match.MatchErr == ErrMethodMismatch && r.handler != nil {
|
||||
// We found a route which matches request method, clear MatchErr
|
||||
match.MatchErr = nil
|
||||
// Then override the mis-matched handler
|
||||
match.Handler = r.handler
|
||||
}
|
||||
|
||||
// Yay, we have a match. Let's collect some info about it.
|
||||
if match.Route == nil {
|
||||
match.Route = r
|
||||
}
|
||||
if match.Handler == nil {
|
||||
match.Handler = r.handler
|
||||
}
|
||||
if match.Vars == nil {
|
||||
match.Vars = make(map[string]string)
|
||||
}
|
||||
|
||||
// Set variables.
|
||||
r.regexp.setMatch(req, match, r)
|
||||
return true
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Route attributes
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// GetError returns an error resulted from building the route, if any.
|
||||
func (r *Route) GetError() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
// BuildOnly sets the route to never match: it is only used to build URLs.
|
||||
func (r *Route) BuildOnly() *Route {
|
||||
r.buildOnly = true
|
||||
return r
|
||||
}
|
||||
|
||||
// Handler --------------------------------------------------------------------
|
||||
|
||||
// Handler sets a handler for the route.
|
||||
func (r *Route) Handler(handler http.Handler) *Route {
|
||||
if r.err == nil {
|
||||
r.handler = handler
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// HandlerFunc sets a handler function for the route.
|
||||
func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route {
|
||||
return r.Handler(http.HandlerFunc(f))
|
||||
}
|
||||
|
||||
// GetHandler returns the handler for the route, if any.
|
||||
func (r *Route) GetHandler() http.Handler {
|
||||
return r.handler
|
||||
}
|
||||
|
||||
// Name -----------------------------------------------------------------------
|
||||
|
||||
// Name sets the name for the route, used to build URLs.
|
||||
// It is an error to call Name more than once on a route.
|
||||
func (r *Route) Name(name string) *Route {
|
||||
if r.name != "" {
|
||||
r.err = fmt.Errorf("mux: route already has name %q, can't set %q",
|
||||
r.name, name)
|
||||
}
|
||||
if r.err == nil {
|
||||
r.name = name
|
||||
r.namedRoutes[name] = r
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// GetName returns the name for the route, if any.
|
||||
func (r *Route) GetName() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Matchers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// matcher types try to match a request.
|
||||
type matcher interface {
|
||||
Match(*http.Request, *RouteMatch) bool
|
||||
}
|
||||
|
||||
// addMatcher adds a matcher to the route.
|
||||
func (r *Route) addMatcher(m matcher) *Route {
|
||||
if r.err == nil {
|
||||
r.matchers = append(r.matchers, m)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// addRegexpMatcher adds a host or path matcher and builder to a route.
|
||||
func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error {
|
||||
if r.err != nil {
|
||||
return r.err
|
||||
}
|
||||
if typ == regexpTypePath || typ == regexpTypePrefix {
|
||||
if len(tpl) > 0 && tpl[0] != '/' {
|
||||
return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
|
||||
}
|
||||
if r.regexp.path != nil {
|
||||
tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
|
||||
}
|
||||
}
|
||||
rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{
|
||||
strictSlash: r.strictSlash,
|
||||
useEncodedPath: r.useEncodedPath,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, q := range r.regexp.queries {
|
||||
if err = uniqueVars(rr.varsN, q.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if typ == regexpTypeHost {
|
||||
if r.regexp.path != nil {
|
||||
if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.regexp.host = rr
|
||||
} else {
|
||||
if r.regexp.host != nil {
|
||||
if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if typ == regexpTypeQuery {
|
||||
r.regexp.queries = append(r.regexp.queries, rr)
|
||||
} else {
|
||||
r.regexp.path = rr
|
||||
}
|
||||
}
|
||||
r.addMatcher(rr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Headers --------------------------------------------------------------------
|
||||
|
||||
// headerMatcher matches the request against header values.
|
||||
type headerMatcher map[string]string
|
||||
|
||||
func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchMapWithString(m, r.Header, true)
|
||||
}
|
||||
|
||||
// Headers adds a matcher for request header values.
|
||||
// It accepts a sequence of key/value pairs to be matched. For example:
|
||||
//
|
||||
// r := mux.NewRouter().NewRoute()
|
||||
// r.Headers("Content-Type", "application/json",
|
||||
// "X-Requested-With", "XMLHttpRequest")
|
||||
//
|
||||
// The above route will only match if both request header values match.
|
||||
// If the value is an empty string, it will match any value if the key is set.
|
||||
func (r *Route) Headers(pairs ...string) *Route {
|
||||
if r.err == nil {
|
||||
var headers map[string]string
|
||||
headers, r.err = mapFromPairsToString(pairs...)
|
||||
return r.addMatcher(headerMatcher(headers))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// headerRegexMatcher matches the request against the route given a regex for the header
|
||||
type headerRegexMatcher map[string]*regexp.Regexp
|
||||
|
||||
func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchMapWithRegex(m, r.Header, true)
|
||||
}
|
||||
|
||||
// HeadersRegexp accepts a sequence of key/value pairs, where the value has regex
|
||||
// support. For example:
|
||||
//
|
||||
// r := mux.NewRouter().NewRoute()
|
||||
// r.HeadersRegexp("Content-Type", "application/(text|json)",
|
||||
// "X-Requested-With", "XMLHttpRequest")
|
||||
//
|
||||
// The above route will only match if both the request header matches both regular expressions.
|
||||
// If the value is an empty string, it will match any value if the key is set.
|
||||
// Use the start and end of string anchors (^ and $) to match an exact value.
|
||||
func (r *Route) HeadersRegexp(pairs ...string) *Route {
|
||||
if r.err == nil {
|
||||
var headers map[string]*regexp.Regexp
|
||||
headers, r.err = mapFromPairsToRegex(pairs...)
|
||||
return r.addMatcher(headerRegexMatcher(headers))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Host -----------------------------------------------------------------------
|
||||
|
||||
// Host adds a matcher for the URL host.
|
||||
// It accepts a template with zero or more URL variables enclosed by {}.
|
||||
// Variables can define an optional regexp pattern to be matched:
|
||||
//
|
||||
// - {name} matches anything until the next dot.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter().NewRoute()
|
||||
// r.Host("www.example.com")
|
||||
// r.Host("{subdomain}.domain.com")
|
||||
// r.Host("{subdomain:[a-z]+}.domain.com")
|
||||
//
|
||||
// Variable names must be unique in a given route. They can be retrieved
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Host(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, regexpTypeHost)
|
||||
return r
|
||||
}
|
||||
|
||||
// MatcherFunc ----------------------------------------------------------------
|
||||
|
||||
// MatcherFunc is the function signature used by custom matchers.
|
||||
type MatcherFunc func(*http.Request, *RouteMatch) bool
|
||||
|
||||
// Match returns the match for a given request.
|
||||
func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return m(r, match)
|
||||
}
|
||||
|
||||
// MatcherFunc adds a custom function to be used as request matcher.
|
||||
func (r *Route) MatcherFunc(f MatcherFunc) *Route {
|
||||
return r.addMatcher(f)
|
||||
}
|
||||
|
||||
// Methods --------------------------------------------------------------------
|
||||
|
||||
// methodMatcher matches the request against HTTP methods.
|
||||
type methodMatcher []string
|
||||
|
||||
func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
return matchInArray(m, r.Method)
|
||||
}
|
||||
|
||||
// Methods adds a matcher for HTTP methods.
|
||||
// It accepts a sequence of one or more methods to be matched, e.g.:
|
||||
// "GET", "POST", "PUT".
|
||||
func (r *Route) Methods(methods ...string) *Route {
|
||||
for k, v := range methods {
|
||||
methods[k] = strings.ToUpper(v)
|
||||
}
|
||||
return r.addMatcher(methodMatcher(methods))
|
||||
}
|
||||
|
||||
// Path -----------------------------------------------------------------------
|
||||
|
||||
// Path adds a matcher for the URL path.
|
||||
// It accepts a template with zero or more URL variables enclosed by {}. The
|
||||
// template must start with a "/".
|
||||
// Variables can define an optional regexp pattern to be matched:
|
||||
//
|
||||
// - {name} matches anything until the next slash.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter().NewRoute()
|
||||
// r.Path("/products/").Handler(ProductsHandler)
|
||||
// r.Path("/products/{key}").Handler(ProductsHandler)
|
||||
// r.Path("/articles/{category}/{id:[0-9]+}").
|
||||
// Handler(ArticleHandler)
|
||||
//
|
||||
// Variable names must be unique in a given route. They can be retrieved
|
||||
// calling mux.Vars(request).
|
||||
func (r *Route) Path(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, regexpTypePath)
|
||||
return r
|
||||
}
|
||||
|
||||
// PathPrefix -----------------------------------------------------------------
|
||||
|
||||
// PathPrefix adds a matcher for the URL path prefix. This matches if the given
|
||||
// template is a prefix of the full URL path. See Route.Path() for details on
|
||||
// the tpl argument.
|
||||
//
|
||||
// Note that it does not treat slashes specially ("/foobar/" will be matched by
|
||||
// the prefix "/foo") so you may want to use a trailing slash here.
|
||||
//
|
||||
// Also note that the setting of Router.StrictSlash() has no effect on routes
|
||||
// with a PathPrefix matcher.
|
||||
func (r *Route) PathPrefix(tpl string) *Route {
|
||||
r.err = r.addRegexpMatcher(tpl, regexpTypePrefix)
|
||||
return r
|
||||
}
|
||||
|
||||
// Query ----------------------------------------------------------------------
|
||||
|
||||
// Queries adds a matcher for URL query values.
|
||||
// It accepts a sequence of key/value pairs. Values may define variables.
|
||||
// For example:
|
||||
//
|
||||
// r := mux.NewRouter().NewRoute()
|
||||
// r.Queries("foo", "bar", "id", "{id:[0-9]+}")
|
||||
//
|
||||
// The above route will only match if the URL contains the defined queries
|
||||
// values, e.g.: ?foo=bar&id=42.
|
||||
//
|
||||
// If the value is an empty string, it will match any value if the key is set.
|
||||
//
|
||||
// Variables can define an optional regexp pattern to be matched:
|
||||
//
|
||||
// - {name} matches anything until the next slash.
|
||||
//
|
||||
// - {name:pattern} matches the given regexp pattern.
|
||||
func (r *Route) Queries(pairs ...string) *Route {
|
||||
length := len(pairs)
|
||||
if length%2 != 0 {
|
||||
r.err = fmt.Errorf(
|
||||
"mux: number of parameters must be multiple of 2, got %v", pairs)
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < length; i += 2 {
|
||||
if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], regexpTypeQuery); r.err != nil {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Schemes --------------------------------------------------------------------
|
||||
|
||||
// schemeMatcher matches the request against URL schemes.
|
||||
type schemeMatcher []string
|
||||
|
||||
func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
||||
scheme := r.URL.Scheme
|
||||
// https://golang.org/pkg/net/http/#Request
|
||||
// "For [most] server requests, fields other than Path and RawQuery will be
|
||||
// empty."
|
||||
// Since we're an http muxer, the scheme is either going to be http or https
|
||||
// though, so we can just set it based on the tls termination state.
|
||||
if scheme == "" {
|
||||
if r.TLS == nil {
|
||||
scheme = "http"
|
||||
} else {
|
||||
scheme = "https"
|
||||
}
|
||||
}
|
||||
return matchInArray(m, scheme)
|
||||
}
|
||||
|
||||
// Schemes adds a matcher for URL schemes.
|
||||
// It accepts a sequence of schemes to be matched, e.g.: "http", "https".
|
||||
// If the request's URL has a scheme set, it will be matched against.
|
||||
// Generally, the URL scheme will only be set if a previous handler set it,
|
||||
// such as the ProxyHeaders handler from gorilla/handlers.
|
||||
// If unset, the scheme will be determined based on the request's TLS
|
||||
// termination state.
|
||||
// The first argument to Schemes will be used when constructing a route URL.
|
||||
func (r *Route) Schemes(schemes ...string) *Route {
|
||||
for k, v := range schemes {
|
||||
schemes[k] = strings.ToLower(v)
|
||||
}
|
||||
if len(schemes) > 0 {
|
||||
r.buildScheme = schemes[0]
|
||||
}
|
||||
return r.addMatcher(schemeMatcher(schemes))
|
||||
}
|
||||
|
||||
// BuildVarsFunc --------------------------------------------------------------
|
||||
|
||||
// BuildVarsFunc is the function signature used by custom build variable
|
||||
// functions (which can modify route variables before a route's URL is built).
|
||||
type BuildVarsFunc func(map[string]string) map[string]string
|
||||
|
||||
// BuildVarsFunc adds a custom function to be used to modify build variables
|
||||
// before a route's URL is built.
|
||||
func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route {
|
||||
if r.buildVarsFunc != nil {
|
||||
// compose the old and new functions
|
||||
old := r.buildVarsFunc
|
||||
r.buildVarsFunc = func(m map[string]string) map[string]string {
|
||||
return f(old(m))
|
||||
}
|
||||
} else {
|
||||
r.buildVarsFunc = f
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Subrouter ------------------------------------------------------------------
|
||||
|
||||
// Subrouter creates a subrouter for the route.
|
||||
//
|
||||
// It will test the inner routes only if the parent route matched. For example:
|
||||
//
|
||||
// r := mux.NewRouter().NewRoute()
|
||||
// s := r.Host("www.example.com").Subrouter()
|
||||
// s.HandleFunc("/products/", ProductsHandler)
|
||||
// s.HandleFunc("/products/{key}", ProductHandler)
|
||||
// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler)
|
||||
//
|
||||
// Here, the routes registered in the subrouter won't be tested if the host
|
||||
// doesn't match.
|
||||
func (r *Route) Subrouter() *Router {
|
||||
// initialize a subrouter with a copy of the parent route's configuration
|
||||
router := &Router{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes}
|
||||
r.addMatcher(router)
|
||||
return router
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// URL building
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// URL builds a URL for the route.
|
||||
//
|
||||
// It accepts a sequence of key/value pairs for the route variables. For
|
||||
// example, given this route:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
// Name("article")
|
||||
//
|
||||
// ...a URL for it can be built using:
|
||||
//
|
||||
// url, err := r.Get("article").URL("category", "technology", "id", "42")
|
||||
//
|
||||
// ...which will return an url.URL with the following path:
|
||||
//
|
||||
// "/articles/technology/42"
|
||||
//
|
||||
// This also works for host variables:
|
||||
//
|
||||
// r := mux.NewRouter()
|
||||
// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).
|
||||
// Host("{subdomain}.domain.com").
|
||||
// Name("article")
|
||||
//
|
||||
// // url.String() will be "http://news.domain.com/articles/technology/42"
|
||||
// url, err := r.Get("article").URL("subdomain", "news",
|
||||
// "category", "technology",
|
||||
// "id", "42")
|
||||
//
|
||||
// The scheme of the resulting url will be the first argument that was passed to Schemes:
|
||||
//
|
||||
// // url.String() will be "https://example.com"
|
||||
// r := mux.NewRouter().NewRoute()
|
||||
// url, err := r.Host("example.com")
|
||||
// .Schemes("https", "http").URL()
|
||||
//
|
||||
// All variables defined in the route are required, and their values must
|
||||
// conform to the corresponding patterns.
|
||||
func (r *Route) URL(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
values, err := r.prepareVars(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var scheme, host, path string
|
||||
queries := make([]string, 0, len(r.regexp.queries))
|
||||
if r.regexp.host != nil {
|
||||
if host, err = r.regexp.host.url(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scheme = "http"
|
||||
if r.buildScheme != "" {
|
||||
scheme = r.buildScheme
|
||||
}
|
||||
}
|
||||
if r.regexp.path != nil {
|
||||
if path, err = r.regexp.path.url(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, q := range r.regexp.queries {
|
||||
var query string
|
||||
if query, err = q.url(values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries = append(queries, query)
|
||||
}
|
||||
return &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: path,
|
||||
RawQuery: strings.Join(queries, "&"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// URLHost builds the host part of the URL for a route. See Route.URL().
|
||||
//
|
||||
// The route must have a host defined.
|
||||
func (r *Route) URLHost(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp.host == nil {
|
||||
return nil, errors.New("mux: route doesn't have a host")
|
||||
}
|
||||
values, err := r.prepareVars(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host, err := r.regexp.host.url(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
}
|
||||
if r.buildScheme != "" {
|
||||
u.Scheme = r.buildScheme
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// URLPath builds the path part of the URL for a route. See Route.URL().
|
||||
//
|
||||
// The route must have a path defined.
|
||||
func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp.path == nil {
|
||||
return nil, errors.New("mux: route doesn't have a path")
|
||||
}
|
||||
values, err := r.prepareVars(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err := r.regexp.path.url(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &url.URL{
|
||||
Path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPathTemplate returns the template used to build the
|
||||
// route match.
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if the route does not define a path.
|
||||
func (r *Route) GetPathTemplate() (string, error) {
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
if r.regexp.path == nil {
|
||||
return "", errors.New("mux: route doesn't have a path")
|
||||
}
|
||||
return r.regexp.path.template, nil
|
||||
}
|
||||
|
||||
// GetPathRegexp returns the expanded regular expression used to match route path.
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if the route does not define a path.
|
||||
func (r *Route) GetPathRegexp() (string, error) {
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
if r.regexp.path == nil {
|
||||
return "", errors.New("mux: route does not have a path")
|
||||
}
|
||||
return r.regexp.path.regexp.String(), nil
|
||||
}
|
||||
|
||||
// GetQueriesRegexp returns the expanded regular expressions used to match the
|
||||
// route queries.
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if the route does not have queries.
|
||||
func (r *Route) GetQueriesRegexp() ([]string, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp.queries == nil {
|
||||
return nil, errors.New("mux: route doesn't have queries")
|
||||
}
|
||||
queries := make([]string, 0, len(r.regexp.queries))
|
||||
for _, query := range r.regexp.queries {
|
||||
queries = append(queries, query.regexp.String())
|
||||
}
|
||||
return queries, nil
|
||||
}
|
||||
|
||||
// GetQueriesTemplates returns the templates used to build the
|
||||
// query matching.
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if the route does not define queries.
|
||||
func (r *Route) GetQueriesTemplates() ([]string, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.regexp.queries == nil {
|
||||
return nil, errors.New("mux: route doesn't have queries")
|
||||
}
|
||||
queries := make([]string, 0, len(r.regexp.queries))
|
||||
for _, query := range r.regexp.queries {
|
||||
queries = append(queries, query.template)
|
||||
}
|
||||
return queries, nil
|
||||
}
|
||||
|
||||
// GetMethods returns the methods the route matches against
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if route does not have methods.
|
||||
func (r *Route) GetMethods() ([]string, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
for _, m := range r.matchers {
|
||||
if methods, ok := m.(methodMatcher); ok {
|
||||
return []string(methods), nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("mux: route doesn't have methods")
|
||||
}
|
||||
|
||||
// GetHostTemplate returns the template used to build the
|
||||
// route match.
|
||||
// This is useful for building simple REST API documentation and for instrumentation
|
||||
// against third-party services.
|
||||
// An error will be returned if the route does not define a host.
|
||||
func (r *Route) GetHostTemplate() (string, error) {
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
if r.regexp.host == nil {
|
||||
return "", errors.New("mux: route doesn't have a host")
|
||||
}
|
||||
return r.regexp.host.template, nil
|
||||
}
|
||||
|
||||
// GetVarNames returns the names of all variables added by regexp matchers
|
||||
// These can be used to know which route variables should be passed into r.URL()
|
||||
func (r *Route) GetVarNames() ([]string, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
var varNames []string
|
||||
if r.regexp.host != nil {
|
||||
varNames = append(varNames, r.regexp.host.varsN...)
|
||||
}
|
||||
if r.regexp.path != nil {
|
||||
varNames = append(varNames, r.regexp.path.varsN...)
|
||||
}
|
||||
for _, regx := range r.regexp.queries {
|
||||
varNames = append(varNames, regx.varsN...)
|
||||
}
|
||||
return varNames, nil
|
||||
}
|
||||
|
||||
// prepareVars converts the route variable pairs into a map. If the route has a
|
||||
// BuildVarsFunc, it is invoked.
|
||||
func (r *Route) prepareVars(pairs ...string) (map[string]string, error) {
|
||||
m, err := mapFromPairsToString(pairs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.buildVars(m), nil
|
||||
}
|
||||
|
||||
func (r *Route) buildVars(m map[string]string) map[string]string {
|
||||
if r.buildVarsFunc != nil {
|
||||
m = r.buildVarsFunc(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright 2012 The Gorilla Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mux
|
||||
|
||||
import "net/http"
|
||||
|
||||
// SetURLVars sets the URL variables for the given request, to be accessed via
|
||||
// mux.Vars for testing route behaviour. Arguments are not modified, a shallow
|
||||
// copy is returned.
|
||||
//
|
||||
// This API should only be used for testing purposes; it provides a way to
|
||||
// inject variables into the request context. Alternatively, URL variables
|
||||
// can be set by making a route that captures the required variables,
|
||||
// starting a server and sending the request to that server.
|
||||
func SetURLVars(r *http.Request, val map[string]string) *http.Request {
|
||||
return requestWithVars(r, val)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
bin/
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
GOBASE=$(shell pwd)
|
||||
GOBIN=$(GOBASE)/bin
|
||||
|
||||
help:
|
||||
@echo "This is a helper makefile for oapi-codegen"
|
||||
@echo "Targets:"
|
||||
@echo " generate: regenerate all generated files"
|
||||
@echo " test: run all tests"
|
||||
@echo " gin_example generate gin example server code"
|
||||
@echo " tidy tidy go mod"
|
||||
|
||||
$(GOBIN)/golangci-lint:
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v1.59.0
|
||||
|
||||
.PHONY: tools
|
||||
tools: $(GOBIN)/golangci-lint
|
||||
|
||||
lint: tools
|
||||
$(GOBIN)/golangci-lint run ./...
|
||||
|
||||
lint-ci: tools
|
||||
$(GOBIN)/golangci-lint run ./... --out-format=github-actions --timeout=5m
|
||||
|
||||
generate:
|
||||
go generate ./...
|
||||
|
||||
test:
|
||||
go test -cover ./...
|
||||
|
||||
tidy:
|
||||
@echo "tidy..."
|
||||
go mod tidy
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# Echo Middleware
|
||||
|
||||
⚠️ This README may be for the latest development version, which may contain unreleased changes. Please ensure you're looking at the README for the latest release version.
|
||||
|
||||
Middleware for the [Echo web server](https://github.com/labstack/echo) for use with [deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen).
|
||||
|
||||
Licensed under the Apache-2.0.
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
// Copyright 2019 DeepMap, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package echomiddleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/getkin/kin-openapi/openapi3filter"
|
||||
"github.com/getkin/kin-openapi/routers"
|
||||
"github.com/getkin/kin-openapi/routers/gorillamux"
|
||||
"github.com/labstack/echo/v4"
|
||||
echomiddleware "github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
const (
|
||||
EchoContextKey = "oapi-codegen/echo-context"
|
||||
UserDataKey = "oapi-codegen/user-data"
|
||||
)
|
||||
|
||||
// OapiValidatorFromYamlFile is an Echo middleware function which validates incoming HTTP requests
|
||||
// to make sure that they conform to the given OAPI 3.0 specification. When
|
||||
// OAPI validation fails on the request, we return an HTTP/400.
|
||||
// Create validator middleware from a YAML file path
|
||||
func OapiValidatorFromYamlFile(path string) (echo.MiddlewareFunc, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %w", path, err)
|
||||
}
|
||||
|
||||
swagger, err := openapi3.NewLoader().LoadFromData(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing %s as Swagger YAML: %w", path, err)
|
||||
}
|
||||
return OapiRequestValidator(swagger), nil
|
||||
}
|
||||
|
||||
// OapiRequestValidator creates a validator from a swagger object.
|
||||
func OapiRequestValidator(swagger *openapi3.T) echo.MiddlewareFunc {
|
||||
return OapiRequestValidatorWithOptions(swagger, nil)
|
||||
}
|
||||
|
||||
// ErrorHandler is called when there is an error in validation
|
||||
type ErrorHandler func(c echo.Context, err *echo.HTTPError) error
|
||||
|
||||
// MultiErrorHandler is called when oapi returns a MultiError type
|
||||
type MultiErrorHandler func(openapi3.MultiError) *echo.HTTPError
|
||||
|
||||
// Options to customize request validation. These are passed through to
|
||||
// openapi3filter.
|
||||
type Options struct {
|
||||
ErrorHandler ErrorHandler
|
||||
Options openapi3filter.Options
|
||||
ParamDecoder openapi3filter.ContentParameterDecoder
|
||||
UserData interface{}
|
||||
Skipper echomiddleware.Skipper
|
||||
MultiErrorHandler MultiErrorHandler
|
||||
// SilenceServersWarning allows silencing a warning for https://github.com/deepmap/oapi-codegen/issues/882 that reports when an OpenAPI spec has `spec.Servers != nil`
|
||||
SilenceServersWarning bool
|
||||
}
|
||||
|
||||
// OapiRequestValidatorWithOptions creates a validator from a swagger object, with validation options
|
||||
func OapiRequestValidatorWithOptions(swagger *openapi3.T, options *Options) echo.MiddlewareFunc {
|
||||
if swagger.Servers != nil && (options == nil || !options.SilenceServersWarning) {
|
||||
log.Println("WARN: OapiRequestValidatorWithOptions called with an OpenAPI spec that has `Servers` set. This may lead to an HTTP 400 with `no matching operation was found` when sending a valid request, as the validator performs `Host` header validation. If you're expecting `Host` header validation, you can silence this warning by setting `Options.SilenceServersWarning = true`. See https://github.com/deepmap/oapi-codegen/issues/882 for more information.")
|
||||
}
|
||||
|
||||
router, err := gorillamux.NewRouter(swagger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
skipper := getSkipperFromOptions(options)
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if skipper(c) {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
err := ValidateRequestFromContext(c, router, options)
|
||||
if err != nil {
|
||||
if options != nil && options.ErrorHandler != nil {
|
||||
return options.ErrorHandler(c, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateRequestFromContext is called from the middleware above and actually does the work
|
||||
// of validating a request.
|
||||
func ValidateRequestFromContext(ctx echo.Context, router routers.Router, options *Options) *echo.HTTPError {
|
||||
req := ctx.Request()
|
||||
route, pathParams, err := router.FindRoute(req)
|
||||
|
||||
// We failed to find a matching route for the request.
|
||||
if err != nil {
|
||||
switch e := err.(type) {
|
||||
case *routers.RouteError:
|
||||
// We've got a bad request, the path requested doesn't match
|
||||
// either server, or path, or something.
|
||||
return echo.NewHTTPError(http.StatusNotFound, e.Reason)
|
||||
default:
|
||||
// This should never happen today, but if our upstream code changes,
|
||||
// we don't want to crash the server, so handle the unexpected error.
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
fmt.Sprintf("error validating route: %s", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
validationInput := &openapi3filter.RequestValidationInput{
|
||||
Request: req,
|
||||
PathParams: pathParams,
|
||||
Route: route,
|
||||
}
|
||||
|
||||
// Pass the Echo context into the request validator, so that any callbacks
|
||||
// which it invokes make it available.
|
||||
requestContext := context.WithValue(context.Background(), EchoContextKey, ctx) //nolint:staticcheck
|
||||
|
||||
if options != nil {
|
||||
validationInput.Options = &options.Options
|
||||
validationInput.ParamDecoder = options.ParamDecoder
|
||||
requestContext = context.WithValue(requestContext, UserDataKey, options.UserData) //nolint:staticcheck
|
||||
}
|
||||
|
||||
err = openapi3filter.ValidateRequest(requestContext, validationInput)
|
||||
if err != nil {
|
||||
me := openapi3.MultiError{}
|
||||
if errors.As(err, &me) {
|
||||
errFunc := getMultiErrorHandlerFromOptions(options)
|
||||
return errFunc(me)
|
||||
}
|
||||
|
||||
switch e := err.(type) {
|
||||
case *openapi3filter.RequestError:
|
||||
// We've got a bad request
|
||||
// Split up the verbose error by lines and return the first one
|
||||
// openapi errors seem to be multi-line with a decent message on the first
|
||||
errorLines := strings.Split(e.Error(), "\n")
|
||||
return &echo.HTTPError{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: errorLines[0],
|
||||
Internal: err,
|
||||
}
|
||||
case *openapi3filter.SecurityRequirementsError:
|
||||
for _, err := range e.Errors {
|
||||
httpErr, ok := err.(*echo.HTTPError)
|
||||
if ok {
|
||||
return httpErr
|
||||
}
|
||||
}
|
||||
return &echo.HTTPError{
|
||||
Code: http.StatusForbidden,
|
||||
Message: e.Error(),
|
||||
Internal: err,
|
||||
}
|
||||
default:
|
||||
// This should never happen today, but if our upstream code changes,
|
||||
// we don't want to crash the server, so handle the unexpected error.
|
||||
return &echo.HTTPError{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: fmt.Sprintf("error validating request: %s", err),
|
||||
Internal: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEchoContext gets the echo context from within requests. It returns
|
||||
// nil if not found or wrong type.
|
||||
func GetEchoContext(c context.Context) echo.Context {
|
||||
iface := c.Value(EchoContextKey)
|
||||
if iface == nil {
|
||||
return nil
|
||||
}
|
||||
eCtx, ok := iface.(echo.Context)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return eCtx
|
||||
}
|
||||
|
||||
func GetUserData(c context.Context) interface{} {
|
||||
return c.Value(UserDataKey)
|
||||
}
|
||||
|
||||
// attempt to get the skipper from the options whether it is set or not
|
||||
func getSkipperFromOptions(options *Options) echomiddleware.Skipper {
|
||||
if options == nil {
|
||||
return echomiddleware.DefaultSkipper
|
||||
}
|
||||
|
||||
if options.Skipper == nil {
|
||||
return echomiddleware.DefaultSkipper
|
||||
}
|
||||
|
||||
return options.Skipper
|
||||
}
|
||||
|
||||
// attempt to get the MultiErrorHandler from the options. If it is not set,
|
||||
// return a default handler
|
||||
func getMultiErrorHandlerFromOptions(options *Options) MultiErrorHandler {
|
||||
if options == nil {
|
||||
return defaultMultiErrorHandler
|
||||
}
|
||||
|
||||
if options.MultiErrorHandler == nil {
|
||||
return defaultMultiErrorHandler
|
||||
}
|
||||
|
||||
return options.MultiErrorHandler
|
||||
}
|
||||
|
||||
// defaultMultiErrorHandler returns a StatusBadRequest (400) and a list
|
||||
// of all of the errors. This method is called if there are no other
|
||||
// methods defined on the options.
|
||||
func defaultMultiErrorHandler(me openapi3.MultiError) *echo.HTTPError {
|
||||
return &echo.HTTPError{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: me.Error(),
|
||||
Internal: me,
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"local>oapi-codegen/renovate-config"
|
||||
]
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
openapi: "3.0.0"
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: TestServer
|
||||
servers:
|
||||
- url: http://deepmap.ai
|
||||
paths:
|
||||
/resource:
|
||||
get:
|
||||
operationId: getResource
|
||||
parameters:
|
||||
- name: id
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 10
|
||||
maximum: 100
|
||||
responses:
|
||||
'200':
|
||||
description: success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
post:
|
||||
operationId: createResource
|
||||
responses:
|
||||
'204':
|
||||
description: No content
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
/protected_resource:
|
||||
get:
|
||||
operationId: getProtectedResource
|
||||
security:
|
||||
- BearerAuth:
|
||||
- someScope
|
||||
responses:
|
||||
'204':
|
||||
description: no content
|
||||
/protected_resource2:
|
||||
get:
|
||||
operationId: getProtectedResource
|
||||
security:
|
||||
- BearerAuth:
|
||||
- otherScope
|
||||
responses:
|
||||
'204':
|
||||
description: no content
|
||||
/protected_resource_401:
|
||||
get:
|
||||
operationId: getProtectedResource
|
||||
security:
|
||||
- BearerAuth:
|
||||
- unauthorized
|
||||
responses:
|
||||
'401':
|
||||
description: no content
|
||||
/multiparamresource:
|
||||
get:
|
||||
operationId: getResource
|
||||
parameters:
|
||||
- name: id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 10
|
||||
maximum: 100
|
||||
- name: id2
|
||||
required: true
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 10
|
||||
maximum: 100
|
||||
responses:
|
||||
'200':
|
||||
description: success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
Reference in New Issue
Block a user