mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
8bbd9f8fc5
full commit from development branch. Examples, book, middleware, plugins are updated to the latest iris version. Read HISTORY.md for more. The 'old' v5 branch which relied on fasthttp exists for those who want to use it navigate there: https://github.com/kataras/iris/tree/5.0.0
1406 lines
42 KiB
Go
1406 lines
42 KiB
Go
package iris
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/geekypanda/httpcache"
|
|
"github.com/iris-contrib/letsencrypt"
|
|
"github.com/kataras/go-errors"
|
|
"golang.org/x/crypto/acme/autocert"
|
|
)
|
|
|
|
const (
|
|
// MethodGet "GET"
|
|
MethodGet = "GET"
|
|
// MethodPost "POST"
|
|
MethodPost = "POST"
|
|
// MethodPut "PUT"
|
|
MethodPut = "PUT"
|
|
// MethodDelete "DELETE"
|
|
MethodDelete = "DELETE"
|
|
// MethodConnect "CONNECT"
|
|
MethodConnect = "CONNECT"
|
|
// MethodHead "HEAD"
|
|
MethodHead = "HEAD"
|
|
// MethodPatch "PATCH"
|
|
MethodPatch = "PATCH"
|
|
// MethodOptions "OPTIONS"
|
|
MethodOptions = "OPTIONS"
|
|
// MethodTrace "TRACE"
|
|
MethodTrace = "TRACE"
|
|
)
|
|
|
|
var (
|
|
// AllMethods "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD", "PATCH", "OPTIONS", "TRACE"
|
|
AllMethods = [...]string{MethodGet, MethodPost, MethodPut, MethodDelete, MethodConnect, MethodHead, MethodPatch, MethodOptions, MethodTrace}
|
|
)
|
|
|
|
// HTTP status codes.
|
|
const (
|
|
StatusContinue = 100 // RFC 7231, 6.2.1
|
|
StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
|
|
StatusProcessing = 102 // RFC 2518, 10.1
|
|
|
|
StatusOK = 200 // RFC 7231, 6.3.1
|
|
StatusCreated = 201 // RFC 7231, 6.3.2
|
|
StatusAccepted = 202 // RFC 7231, 6.3.3
|
|
StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4
|
|
StatusNoContent = 204 // RFC 7231, 6.3.5
|
|
StatusResetContent = 205 // RFC 7231, 6.3.6
|
|
StatusPartialContent = 206 // RFC 7233, 4.1
|
|
StatusMultiStatus = 207 // RFC 4918, 11.1
|
|
StatusAlreadyReported = 208 // RFC 5842, 7.1
|
|
StatusIMUsed = 226 // RFC 3229, 10.4.1
|
|
|
|
StatusMultipleChoices = 300 // RFC 7231, 6.4.1
|
|
StatusMovedPermanently = 301 // RFC 7231, 6.4.2
|
|
StatusFound = 302 // RFC 7231, 6.4.3
|
|
StatusSeeOther = 303 // RFC 7231, 6.4.4
|
|
StatusNotModified = 304 // RFC 7232, 4.1
|
|
StatusUseProxy = 305 // RFC 7231, 6.4.5
|
|
_ = 306 // RFC 7231, 6.4.6 (Unused)
|
|
StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7
|
|
StatusPermanentRedirect = 308 // RFC 7538, 3
|
|
|
|
StatusBadRequest = 400 // RFC 7231, 6.5.1
|
|
StatusUnauthorized = 401 // RFC 7235, 3.1
|
|
StatusPaymentRequired = 402 // RFC 7231, 6.5.2
|
|
StatusForbidden = 403 // RFC 7231, 6.5.3
|
|
StatusNotFound = 404 // RFC 7231, 6.5.4
|
|
StatusMethodNotAllowed = 405 // RFC 7231, 6.5.5
|
|
StatusNotAcceptable = 406 // RFC 7231, 6.5.6
|
|
StatusProxyAuthRequired = 407 // RFC 7235, 3.2
|
|
StatusRequestTimeout = 408 // RFC 7231, 6.5.7
|
|
StatusConflict = 409 // RFC 7231, 6.5.8
|
|
StatusGone = 410 // RFC 7231, 6.5.9
|
|
StatusLengthRequired = 411 // RFC 7231, 6.5.10
|
|
StatusPreconditionFailed = 412 // RFC 7232, 4.2
|
|
StatusRequestEntityTooLarge = 413 // RFC 7231, 6.5.11
|
|
StatusRequestURITooLong = 414 // RFC 7231, 6.5.12
|
|
StatusUnsupportedMediaType = 415 // RFC 7231, 6.5.13
|
|
StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4
|
|
StatusExpectationFailed = 417 // RFC 7231, 6.5.14
|
|
StatusTeapot = 418 // RFC 7168, 2.3.3
|
|
StatusUnprocessableEntity = 422 // RFC 4918, 11.2
|
|
StatusLocked = 423 // RFC 4918, 11.3
|
|
StatusFailedDependency = 424 // RFC 4918, 11.4
|
|
StatusUpgradeRequired = 426 // RFC 7231, 6.5.15
|
|
StatusPreconditionRequired = 428 // RFC 6585, 3
|
|
StatusTooManyRequests = 429 // RFC 6585, 4
|
|
StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5
|
|
StatusUnavailableForLegalReasons = 451 // RFC 7725, 3
|
|
|
|
StatusInternalServerError = 500 // RFC 7231, 6.6.1
|
|
StatusNotImplemented = 501 // RFC 7231, 6.6.2
|
|
StatusBadGateway = 502 // RFC 7231, 6.6.3
|
|
StatusServiceUnavailable = 503 // RFC 7231, 6.6.4
|
|
StatusGatewayTimeout = 504 // RFC 7231, 6.6.5
|
|
StatusHTTPVersionNotSupported = 505 // RFC 7231, 6.6.6
|
|
StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1
|
|
StatusInsufficientStorage = 507 // RFC 4918, 11.5
|
|
StatusLoopDetected = 508 // RFC 5842, 7.2
|
|
StatusNotExtended = 510 // RFC 2774, 7
|
|
StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6
|
|
)
|
|
|
|
var statusText = map[int]string{
|
|
StatusContinue: "Continue",
|
|
StatusSwitchingProtocols: "Switching Protocols",
|
|
StatusProcessing: "Processing",
|
|
|
|
StatusOK: "OK",
|
|
StatusCreated: "Created",
|
|
StatusAccepted: "Accepted",
|
|
StatusNonAuthoritativeInfo: "Non-Authoritative Information",
|
|
StatusNoContent: "No Content",
|
|
StatusResetContent: "Reset Content",
|
|
StatusPartialContent: "Partial Content",
|
|
StatusMultiStatus: "Multi-Status",
|
|
StatusAlreadyReported: "Already Reported",
|
|
StatusIMUsed: "IM Used",
|
|
|
|
StatusMultipleChoices: "Multiple Choices",
|
|
StatusMovedPermanently: "Moved Permanently",
|
|
StatusFound: "Found",
|
|
StatusSeeOther: "See Other",
|
|
StatusNotModified: "Not Modified",
|
|
StatusUseProxy: "Use Proxy",
|
|
StatusTemporaryRedirect: "Temporary Redirect",
|
|
StatusPermanentRedirect: "Permanent Redirect",
|
|
|
|
StatusBadRequest: "Bad Request",
|
|
StatusUnauthorized: "Unauthorized",
|
|
StatusPaymentRequired: "Payment Required",
|
|
StatusForbidden: "Forbidden",
|
|
StatusNotFound: "Not Found",
|
|
StatusMethodNotAllowed: "Method Not Allowed",
|
|
StatusNotAcceptable: "Not Acceptable",
|
|
StatusProxyAuthRequired: "Proxy Authentication Required",
|
|
StatusRequestTimeout: "Request Timeout",
|
|
StatusConflict: "Conflict",
|
|
StatusGone: "Gone",
|
|
StatusLengthRequired: "Length Required",
|
|
StatusPreconditionFailed: "Precondition Failed",
|
|
StatusRequestEntityTooLarge: "Request Entity Too Large",
|
|
StatusRequestURITooLong: "Request URI Too Long",
|
|
StatusUnsupportedMediaType: "Unsupported Media Type",
|
|
StatusRequestedRangeNotSatisfiable: "Requested Range Not Satisfiable",
|
|
StatusExpectationFailed: "Expectation Failed",
|
|
StatusTeapot: "I'm a teapot",
|
|
StatusUnprocessableEntity: "Unprocessable Entity",
|
|
StatusLocked: "Locked",
|
|
StatusFailedDependency: "Failed Dependency",
|
|
StatusUpgradeRequired: "Upgrade Required",
|
|
StatusPreconditionRequired: "Precondition Required",
|
|
StatusTooManyRequests: "Too Many Requests",
|
|
StatusRequestHeaderFieldsTooLarge: "Request Header Fields Too Large",
|
|
StatusUnavailableForLegalReasons: "Unavailable For Legal Reasons",
|
|
|
|
StatusInternalServerError: "Internal Server Error",
|
|
StatusNotImplemented: "Not Implemented",
|
|
StatusBadGateway: "Bad Gateway",
|
|
StatusServiceUnavailable: "Service Unavailable",
|
|
StatusGatewayTimeout: "Gateway Timeout",
|
|
StatusHTTPVersionNotSupported: "HTTP Version Not Supported",
|
|
StatusVariantAlsoNegotiates: "Variant Also Negotiates",
|
|
StatusInsufficientStorage: "Insufficient Storage",
|
|
StatusLoopDetected: "Loop Detected",
|
|
StatusNotExtended: "Not Extended",
|
|
StatusNetworkAuthenticationRequired: "Network Authentication Required",
|
|
}
|
|
|
|
// StatusText returns a text for the HTTP status code. It returns the empty
|
|
// string if the code is unknown.
|
|
func StatusText(code int) string {
|
|
return statusText[code]
|
|
}
|
|
|
|
// errHandler returns na error with message: 'Passed argument is not func(*Context) neither an object which implements the iris.Handler with Serve(ctx *Context)
|
|
// It seems to be a +type Points to: +pointer.'
|
|
var errHandler = errors.New("Passed argument is not func(*Context) neither an object which implements the iris.Handler with Serve(ctx *Context)\n It seems to be a %T Points to: %v.")
|
|
|
|
type (
|
|
// Handler the main Iris Handler interface.
|
|
Handler interface {
|
|
Serve(ctx *Context) // iris-specific
|
|
}
|
|
|
|
// HandlerFunc type is an adapter to allow the use of
|
|
// ordinary functions as HTTP handlers. If f is a function
|
|
// with the appropriate signature, HandlerFunc(f) is a
|
|
// Handler that calls f.
|
|
HandlerFunc func(*Context)
|
|
// Middleware is just a slice of Handler []func(c *Context)
|
|
Middleware []Handler
|
|
|
|
// HandlerAPI empty interface used for .API
|
|
HandlerAPI interface{}
|
|
)
|
|
|
|
// Serve implements the Handler, is like ServeHTTP but for Iris
|
|
func (h HandlerFunc) Serve(ctx *Context) {
|
|
h(ctx)
|
|
}
|
|
|
|
// ToNativeHandler converts an iris handler to http.Handler
|
|
func ToNativeHandler(station *Framework, h Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := station.AcquireCtx(w, r)
|
|
h.Serve(ctx)
|
|
station.ReleaseCtx(ctx)
|
|
})
|
|
}
|
|
|
|
// ToHandler converts an http.Handler/HandlerFunc to iris.Handler(Func)
|
|
func ToHandler(handler interface{}) HandlerFunc {
|
|
|
|
//this is not the best way to do it, but I dont have any options right now.
|
|
switch handler.(type) {
|
|
case HandlerFunc:
|
|
//it's already an iris handler
|
|
return handler.(HandlerFunc)
|
|
case http.Handler:
|
|
//it's http.Handler
|
|
h := handler.(http.Handler)
|
|
return func(ctx *Context) {
|
|
h.ServeHTTP(ctx.ResponseWriter, ctx.Request)
|
|
}
|
|
case func(http.ResponseWriter, *http.Request):
|
|
return ToHandler(http.HandlerFunc(handler.(func(http.ResponseWriter, *http.Request))))
|
|
//for func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc): READ iris-conrib/middleware/README.md for details
|
|
default:
|
|
panic(errHandler.Format(handler, handler))
|
|
}
|
|
|
|
}
|
|
|
|
// convertToHandlers just make []HandlerFunc to []Handler, although HandlerFunc and Handler are the same
|
|
// we need this on some cases we explicit want a interface Handler, it is useless for users.
|
|
func convertToHandlers(handlersFn []HandlerFunc) []Handler {
|
|
hlen := len(handlersFn)
|
|
mlist := make([]Handler, hlen)
|
|
for i := 0; i < hlen; i++ {
|
|
mlist[i] = Handler(handlersFn[i])
|
|
}
|
|
return mlist
|
|
}
|
|
|
|
// joinMiddleware uses to create a copy of all middleware and return them in order to use inside the node
|
|
func joinMiddleware(middleware1 Middleware, middleware2 Middleware) Middleware {
|
|
nowLen := len(middleware1)
|
|
totalLen := nowLen + len(middleware2)
|
|
// create a new slice of middleware in order to store all handlers, the already handlers(middleware) and the new
|
|
newMiddleware := make(Middleware, totalLen)
|
|
//copy the already middleware to the just created
|
|
copy(newMiddleware, middleware1)
|
|
//start from there we finish, and store the new middleware too
|
|
copy(newMiddleware[nowLen:], middleware2)
|
|
return newMiddleware
|
|
}
|
|
|
|
const (
|
|
// parameterStartByte is very used on the node, it's just contains the byte for the ':' rune/char
|
|
parameterStartByte = byte(':')
|
|
// slashByte is just a byte of '/' rune/char
|
|
slashByte = byte('/')
|
|
// slash is just a string of "/"
|
|
slash = "/"
|
|
// matchEverythingByte is just a byte of '*" rune/char
|
|
matchEverythingByte = byte('*')
|
|
|
|
isRoot entryCase = iota
|
|
hasParams
|
|
matchEverything
|
|
)
|
|
|
|
type (
|
|
// entryCase is the type which the type of muxEntryusing in order to determinate what type (parameterized, anything, static...) is the perticular node
|
|
entryCase uint8
|
|
|
|
// muxEntry is the node of a tree of the routes,
|
|
// in order to learn how this is working, google 'trie' or watch this lecture: https://www.youtube.com/watch?v=uhAUk63tLRM
|
|
// this method is used by the BSD's kernel also
|
|
muxEntry struct {
|
|
part string
|
|
entryCase entryCase
|
|
hasWildNode bool
|
|
tokens string
|
|
nodes []*muxEntry
|
|
middleware Middleware
|
|
precedence uint64
|
|
paramsLen uint8
|
|
}
|
|
)
|
|
|
|
var (
|
|
errMuxEntryConflictsWildcard = errors.New("Router: Path's part: '%s' conflicts with wildcard '%s' in the route path: '%s' !")
|
|
errMuxEntryMiddlewareAlreadyExists = errors.New("Router: Middleware were already registered for the path: '%s' !")
|
|
errMuxEntryInvalidWildcard = errors.New("Router: More than one wildcard found in the path part: '%s' in route's path: '%s' !")
|
|
errMuxEntryConflictsExistingWildcard = errors.New("Router: Wildcard for route path: '%s' conflicts with existing children in route path: '%s' !")
|
|
errMuxEntryWildcardUnnamed = errors.New("Router: Unnamed wildcard found in path: '%s' !")
|
|
errMuxEntryWildcardInvalidPlace = errors.New("Router: Wildcard is only allowed at the end of the path, in the route path: '%s' !")
|
|
errMuxEntryWildcardConflictsMiddleware = errors.New("Router: Wildcard conflicts with existing middleware for the route path: '%s' !")
|
|
errMuxEntryWildcardMissingSlash = errors.New("Router: No slash(/) were found before wildcard in the route path: '%s' !")
|
|
)
|
|
|
|
// getParamsLen returns the parameters length from a given path
|
|
func getParamsLen(path string) uint8 {
|
|
var n uint
|
|
for i := 0; i < len(path); i++ {
|
|
if path[i] != ':' && path[i] != '*' { // ParameterStartByte & MatchEverythingByte
|
|
continue
|
|
}
|
|
n++
|
|
}
|
|
if n >= 255 {
|
|
return 255
|
|
}
|
|
return uint8(n)
|
|
}
|
|
|
|
// findLower returns the smaller number between a and b
|
|
func findLower(a, b int) int {
|
|
if a <= b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// add adds a muxEntry to the existing muxEntry or to the tree if no muxEntry has the prefix of
|
|
func (e *muxEntry) add(path string, middleware Middleware) error {
|
|
fullPath := path
|
|
e.precedence++
|
|
numParams := getParamsLen(path)
|
|
|
|
if len(e.part) > 0 || len(e.nodes) > 0 {
|
|
loop:
|
|
for {
|
|
if numParams > e.paramsLen {
|
|
e.paramsLen = numParams
|
|
}
|
|
|
|
i := 0
|
|
max := findLower(len(path), len(e.part))
|
|
for i < max && path[i] == e.part[i] {
|
|
i++
|
|
}
|
|
|
|
if i < len(e.part) {
|
|
node := muxEntry{
|
|
part: e.part[i:],
|
|
hasWildNode: e.hasWildNode,
|
|
tokens: e.tokens,
|
|
nodes: e.nodes,
|
|
middleware: e.middleware,
|
|
precedence: e.precedence - 1,
|
|
}
|
|
|
|
for i := range node.nodes {
|
|
if node.nodes[i].paramsLen > node.paramsLen {
|
|
node.paramsLen = node.nodes[i].paramsLen
|
|
}
|
|
}
|
|
|
|
e.nodes = []*muxEntry{&node}
|
|
e.tokens = string([]byte{e.part[i]})
|
|
e.part = path[:i]
|
|
e.middleware = nil
|
|
e.hasWildNode = false
|
|
}
|
|
|
|
if i < len(path) {
|
|
path = path[i:]
|
|
|
|
if e.hasWildNode {
|
|
e = e.nodes[0]
|
|
e.precedence++
|
|
|
|
if numParams > e.paramsLen {
|
|
e.paramsLen = numParams
|
|
}
|
|
numParams--
|
|
|
|
if len(path) >= len(e.part) && e.part == path[:len(e.part)] {
|
|
|
|
if len(e.part) >= len(path) || path[len(e.part)] == slashByte {
|
|
continue loop
|
|
}
|
|
}
|
|
return errMuxEntryConflictsWildcard.Format(path, e.part, fullPath)
|
|
}
|
|
|
|
c := path[0]
|
|
|
|
if e.entryCase == hasParams && c == slashByte && len(e.nodes) == 1 {
|
|
e = e.nodes[0]
|
|
e.precedence++
|
|
continue loop
|
|
}
|
|
for i := range e.tokens {
|
|
if c == e.tokens[i] {
|
|
i = e.precedenceTo(i)
|
|
e = e.nodes[i]
|
|
continue loop
|
|
}
|
|
}
|
|
|
|
if c != parameterStartByte && c != matchEverythingByte {
|
|
|
|
e.tokens += string([]byte{c})
|
|
node := &muxEntry{
|
|
paramsLen: numParams,
|
|
}
|
|
e.nodes = append(e.nodes, node)
|
|
e.precedenceTo(len(e.tokens) - 1)
|
|
e = node
|
|
}
|
|
e.addNode(numParams, path, fullPath, middleware)
|
|
return nil
|
|
|
|
} else if i == len(path) {
|
|
if e.middleware != nil {
|
|
return errMuxEntryMiddlewareAlreadyExists.Format(fullPath)
|
|
}
|
|
e.middleware = middleware
|
|
}
|
|
return nil
|
|
}
|
|
} else {
|
|
e.addNode(numParams, path, fullPath, middleware)
|
|
e.entryCase = isRoot
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// addNode adds a muxEntry as children to other muxEntry
|
|
func (e *muxEntry) addNode(numParams uint8, path string, fullPath string, middleware Middleware) error {
|
|
var offset int
|
|
|
|
for i, max := 0, len(path); numParams > 0; i++ {
|
|
c := path[i]
|
|
if c != parameterStartByte && c != matchEverythingByte {
|
|
continue
|
|
}
|
|
|
|
end := i + 1
|
|
for end < max && path[end] != slashByte {
|
|
switch path[end] {
|
|
case parameterStartByte, matchEverythingByte:
|
|
/*
|
|
panic("only one wildcard per path segment is allowed, has: '" +
|
|
path[i:] + "' in path '" + fullPath + "'")
|
|
*/
|
|
return errMuxEntryInvalidWildcard.Format(path[i:], fullPath)
|
|
default:
|
|
end++
|
|
}
|
|
}
|
|
|
|
if len(e.nodes) > 0 {
|
|
return errMuxEntryConflictsExistingWildcard.Format(path[i:end], fullPath)
|
|
}
|
|
|
|
if end-i < 2 {
|
|
return errMuxEntryWildcardUnnamed.Format(fullPath)
|
|
}
|
|
|
|
if c == parameterStartByte {
|
|
|
|
if i > 0 {
|
|
e.part = path[offset:i]
|
|
offset = i
|
|
}
|
|
|
|
child := &muxEntry{
|
|
entryCase: hasParams,
|
|
paramsLen: numParams,
|
|
}
|
|
e.nodes = []*muxEntry{child}
|
|
e.hasWildNode = true
|
|
e = child
|
|
e.precedence++
|
|
numParams--
|
|
|
|
if end < max {
|
|
e.part = path[offset:end]
|
|
offset = end
|
|
|
|
child := &muxEntry{
|
|
paramsLen: numParams,
|
|
precedence: 1,
|
|
}
|
|
e.nodes = []*muxEntry{child}
|
|
e = child
|
|
}
|
|
|
|
} else {
|
|
if end != max || numParams > 1 {
|
|
return errMuxEntryWildcardInvalidPlace.Format(fullPath)
|
|
}
|
|
|
|
if len(e.part) > 0 && e.part[len(e.part)-1] == '/' {
|
|
return errMuxEntryWildcardConflictsMiddleware.Format(fullPath)
|
|
}
|
|
|
|
i--
|
|
if path[i] != slashByte {
|
|
return errMuxEntryWildcardMissingSlash.Format(fullPath)
|
|
}
|
|
|
|
e.part = path[offset:i]
|
|
|
|
child := &muxEntry{
|
|
hasWildNode: true,
|
|
entryCase: matchEverything,
|
|
paramsLen: 1,
|
|
}
|
|
e.nodes = []*muxEntry{child}
|
|
e.tokens = string(path[i])
|
|
e = child
|
|
e.precedence++
|
|
|
|
child = &muxEntry{
|
|
part: path[i:],
|
|
entryCase: matchEverything,
|
|
paramsLen: 1,
|
|
middleware: middleware,
|
|
precedence: 1,
|
|
}
|
|
e.nodes = []*muxEntry{child}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
e.part = path[offset:]
|
|
e.middleware = middleware
|
|
|
|
return nil
|
|
}
|
|
|
|
// get is used by the Router, it finds and returns the correct muxEntry for a path
|
|
func (e *muxEntry) get(path string, ctx *Context) (mustRedirect bool) {
|
|
loop:
|
|
for {
|
|
if len(path) > len(e.part) {
|
|
if path[:len(e.part)] == e.part {
|
|
path = path[len(e.part):]
|
|
|
|
if !e.hasWildNode {
|
|
c := path[0]
|
|
for i := range e.tokens {
|
|
if c == e.tokens[i] {
|
|
e = e.nodes[i]
|
|
continue loop
|
|
}
|
|
}
|
|
|
|
mustRedirect = (path == slash && e.middleware != nil)
|
|
return
|
|
}
|
|
|
|
e = e.nodes[0]
|
|
switch e.entryCase {
|
|
case hasParams:
|
|
|
|
end := 0
|
|
for end < len(path) && path[end] != '/' {
|
|
end++
|
|
}
|
|
|
|
ctx.Set(e.part[1:], path[:end])
|
|
|
|
if end < len(path) {
|
|
if len(e.nodes) > 0 {
|
|
path = path[end:]
|
|
e = e.nodes[0]
|
|
continue loop
|
|
}
|
|
|
|
mustRedirect = (len(path) == end+1)
|
|
return
|
|
}
|
|
if ctx.Middleware = e.middleware; ctx.Middleware != nil {
|
|
return
|
|
} else if len(e.nodes) == 1 {
|
|
e = e.nodes[0]
|
|
mustRedirect = (e.part == slash && e.middleware != nil)
|
|
}
|
|
|
|
return
|
|
|
|
case matchEverything:
|
|
|
|
ctx.Set(e.part[2:], path)
|
|
ctx.Middleware = e.middleware
|
|
return
|
|
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
} else if path == e.part {
|
|
if ctx.Middleware = e.middleware; ctx.Middleware != nil {
|
|
return
|
|
}
|
|
|
|
if path == slash && e.hasWildNode && e.entryCase != isRoot {
|
|
mustRedirect = true
|
|
return
|
|
}
|
|
|
|
for i := range e.tokens {
|
|
if e.tokens[i] == slashByte {
|
|
e = e.nodes[i]
|
|
mustRedirect = (len(e.part) == 1 && e.middleware != nil) ||
|
|
(e.entryCase == matchEverything && e.nodes[0].middleware != nil)
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
mustRedirect = (path == slash) ||
|
|
(len(e.part) == len(path)+1 && e.part[len(path)] == slashByte &&
|
|
path == e.part[:len(e.part)-1] && e.middleware != nil)
|
|
return
|
|
}
|
|
}
|
|
|
|
// precedenceTo just adds the priority of this muxEntry by an index
|
|
func (e *muxEntry) precedenceTo(index int) int {
|
|
e.nodes[index].precedence++
|
|
_precedence := e.nodes[index].precedence
|
|
|
|
newindex := index
|
|
for newindex > 0 && e.nodes[newindex-1].precedence < _precedence {
|
|
tmpN := e.nodes[newindex-1]
|
|
e.nodes[newindex-1] = e.nodes[newindex]
|
|
e.nodes[newindex] = tmpN
|
|
|
|
newindex--
|
|
}
|
|
|
|
if newindex != index {
|
|
e.tokens = e.tokens[:newindex] +
|
|
e.tokens[index:index+1] +
|
|
e.tokens[newindex:index] + e.tokens[index+1:]
|
|
}
|
|
|
|
return newindex
|
|
}
|
|
|
|
// cachedMuxEntry is just a wrapper for the Cache functionality
|
|
// it seems useless but I prefer to keep the cached handler on its own memory stack,
|
|
// reason: no clojures hell in the Cache function
|
|
type cachedMuxEntry struct {
|
|
cachedHandler http.Handler
|
|
}
|
|
|
|
func newCachedMuxEntry(s *Framework, bodyHandler HandlerFunc, expiration time.Duration) *cachedMuxEntry {
|
|
httphandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := s.AcquireCtx(w, r)
|
|
bodyHandler.Serve(ctx)
|
|
s.ReleaseCtx(ctx)
|
|
})
|
|
|
|
cachedHandler := httpcache.Cache(httphandler, expiration)
|
|
return &cachedMuxEntry{
|
|
cachedHandler: cachedHandler,
|
|
}
|
|
}
|
|
|
|
func (c *cachedMuxEntry) Serve(ctx *Context) {
|
|
c.cachedHandler.ServeHTTP(ctx.ResponseWriter, ctx.Request)
|
|
}
|
|
|
|
type (
|
|
// Route contains some useful information about a route
|
|
Route interface {
|
|
// Name returns the name of the route
|
|
Name() string
|
|
// Subdomain returns the subdomain,if any
|
|
Subdomain() string
|
|
// Method returns the http method
|
|
Method() string
|
|
// Path returns the path
|
|
Path() string
|
|
// SetPath changes/sets the path for this route
|
|
SetPath(string)
|
|
// Middleware returns the slice of Handler([]Handler) registed to this route
|
|
Middleware() Middleware
|
|
// SetMiddleware changes/sets the middleware(handler(s)) for this route
|
|
SetMiddleware(Middleware)
|
|
}
|
|
|
|
route struct {
|
|
// if no name given then it's the subdomain+path
|
|
name string
|
|
subdomain string
|
|
method string
|
|
path string
|
|
middleware Middleware
|
|
formattedPath string
|
|
formattedParts int
|
|
}
|
|
|
|
bySubdomain []*route
|
|
)
|
|
|
|
// Sorting happens when the mux's request handler initialized
|
|
func (s bySubdomain) Len() int {
|
|
return len(s)
|
|
}
|
|
func (s bySubdomain) Swap(i, j int) {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|
|
func (s bySubdomain) Less(i, j int) bool {
|
|
return len(s[i].Subdomain()) > len(s[j].Subdomain())
|
|
}
|
|
|
|
var _ Route = &route{}
|
|
|
|
func newRoute(method string, subdomain string, path string, middleware Middleware) *route {
|
|
r := &route{name: path + subdomain, method: method, subdomain: subdomain, path: path, middleware: middleware}
|
|
r.formatPath()
|
|
return r
|
|
}
|
|
|
|
func (r *route) formatPath() {
|
|
// we don't care about performance here.
|
|
n1Len := strings.Count(r.path, ":")
|
|
isMatchEverything := len(r.path) > 0 && r.path[len(r.path)-1] == matchEverythingByte
|
|
if n1Len == 0 && !isMatchEverything {
|
|
// its a static
|
|
return
|
|
}
|
|
if n1Len == 0 && isMatchEverything {
|
|
//if we have something like: /mypath/anything/* -> /mypatch/anything/%v
|
|
r.formattedPath = r.path[0:len(r.path)-2] + "%v"
|
|
r.formattedParts++
|
|
return
|
|
}
|
|
|
|
tempPath := r.path
|
|
splittedN1 := strings.Split(r.path, "/")
|
|
|
|
for _, v := range splittedN1 {
|
|
if len(v) > 0 {
|
|
if v[0] == ':' || v[0] == matchEverythingByte {
|
|
r.formattedParts++
|
|
tempPath = strings.Replace(tempPath, v, "%v", -1) // n1Len, but let it we don't care about performance here.
|
|
}
|
|
}
|
|
|
|
}
|
|
r.formattedPath = tempPath
|
|
}
|
|
|
|
func (r *route) setName(newName string) {
|
|
r.name = newName
|
|
}
|
|
|
|
func (r route) Name() string {
|
|
return r.name
|
|
}
|
|
|
|
func (r route) Subdomain() string {
|
|
return r.subdomain
|
|
}
|
|
|
|
func (r route) Method() string {
|
|
return r.method
|
|
}
|
|
|
|
func (r route) Path() string {
|
|
return r.path
|
|
}
|
|
|
|
func (r *route) SetPath(s string) {
|
|
r.path = s
|
|
}
|
|
|
|
func (r route) Middleware() Middleware {
|
|
return r.middleware
|
|
}
|
|
|
|
func (r *route) SetMiddleware(m Middleware) {
|
|
r.middleware = m
|
|
}
|
|
|
|
// RouteConflicts checks for route's middleware conflicts
|
|
func RouteConflicts(r *route, with string) bool {
|
|
for _, h := range r.middleware {
|
|
if m, ok := h.(interface {
|
|
Conflicts() string
|
|
}); ok {
|
|
if c := m.Conflicts(); c == with {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r *route) hasCors() bool {
|
|
return RouteConflicts(r, "httpmethod")
|
|
}
|
|
|
|
const (
|
|
// subdomainIndicator where './' exists in a registed path then it contains subdomain
|
|
subdomainIndicator = "./"
|
|
// dynamicSubdomainIndicator where a registed path starts with '*.' then it contains a dynamic subdomain, if subdomain == "*." then its dynamic
|
|
dynamicSubdomainIndicator = "*."
|
|
)
|
|
|
|
type (
|
|
muxTree struct {
|
|
method string
|
|
// subdomain is empty for default-hostname routes,
|
|
// ex: mysubdomain.
|
|
subdomain string
|
|
entry *muxEntry
|
|
}
|
|
|
|
serveMux struct {
|
|
garden []*muxTree
|
|
lookups []*route
|
|
maxParameters uint8
|
|
|
|
onLookup func(Route)
|
|
|
|
api *muxAPI
|
|
errorHandlers map[int]Handler
|
|
logger *log.Logger
|
|
// the main server host's name, ex: localhost, 127.0.0.1, 0.0.0.0, iris-go.com
|
|
hostname string
|
|
// if any of the trees contains not empty subdomain
|
|
hosts bool
|
|
// if false then the /something it's not the same as /something/
|
|
// defaults to true
|
|
correctPath bool
|
|
// if enabled then the router checks and fires an error for 405 http status method not allowed too if no method compatible method was found
|
|
// by default is false
|
|
fireMethodNotAllowed bool
|
|
mu sync.Mutex
|
|
}
|
|
)
|
|
|
|
func newServeMux(logger *log.Logger) *serveMux {
|
|
mux := &serveMux{
|
|
lookups: make([]*route, 0),
|
|
errorHandlers: make(map[int]Handler, 0),
|
|
hostname: DefaultServerHostname, // these are changing when the server is up
|
|
correctPath: !DefaultDisablePathCorrection,
|
|
fireMethodNotAllowed: false,
|
|
logger: logger,
|
|
}
|
|
|
|
return mux
|
|
}
|
|
|
|
func (mux *serveMux) setHostname(h string) {
|
|
mux.hostname = h
|
|
}
|
|
|
|
func (mux *serveMux) setCorrectPath(b bool) {
|
|
mux.correctPath = b
|
|
}
|
|
|
|
func (mux *serveMux) setFireMethodNotAllowed(b bool) {
|
|
mux.fireMethodNotAllowed = b
|
|
}
|
|
|
|
// registerError registers a handler to a http status
|
|
func (mux *serveMux) registerError(statusCode int, handler Handler) {
|
|
mux.mu.Lock()
|
|
func(statusCode int, handler Handler) {
|
|
mux.errorHandlers[statusCode] = HandlerFunc(func(ctx *Context) {
|
|
ctx.ResetBody()
|
|
ctx.SetStatusCode(statusCode)
|
|
handler.Serve(ctx)
|
|
})
|
|
}(statusCode, handler)
|
|
mux.mu.Unlock()
|
|
}
|
|
|
|
// fireError fires an error
|
|
func (mux *serveMux) fireError(statusCode int, ctx *Context) {
|
|
mux.mu.Lock()
|
|
errHandler := mux.errorHandlers[statusCode]
|
|
if errHandler == nil {
|
|
errHandler = HandlerFunc(func(ctx *Context) {
|
|
ctx.ResponseWriter.Reset()
|
|
ctx.SetStatusCode(statusCode)
|
|
ctx.SetBodyString(statusText[statusCode])
|
|
})
|
|
mux.errorHandlers[statusCode] = errHandler
|
|
}
|
|
mux.mu.Unlock()
|
|
|
|
errHandler.Serve(ctx)
|
|
}
|
|
|
|
func (mux *serveMux) getTree(method string, subdomain string) *muxTree {
|
|
for i := range mux.garden {
|
|
t := mux.garden[i]
|
|
if t.method == method && t.subdomain == subdomain {
|
|
return t
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (mux *serveMux) register(method string, subdomain string, path string, middleware Middleware) *route {
|
|
mux.mu.Lock()
|
|
defer mux.mu.Unlock()
|
|
|
|
if subdomain != "" {
|
|
mux.hosts = true
|
|
}
|
|
|
|
// add to the lookups, it's just a collection of routes information
|
|
lookup := newRoute(method, subdomain, path, middleware)
|
|
if mux.onLookup != nil {
|
|
mux.onLookup(lookup)
|
|
}
|
|
mux.lookups = append(mux.lookups, lookup)
|
|
|
|
return lookup
|
|
|
|
}
|
|
|
|
// build collects all routes info and adds them to the registry in order to be served from the request handler
|
|
// this happens once when server is setting the mux's handler.
|
|
func (mux *serveMux) build() (methodEqual func(string, string) bool) {
|
|
|
|
sort.Sort(bySubdomain(mux.lookups))
|
|
|
|
for i := range mux.lookups {
|
|
r := mux.lookups[i]
|
|
// add to the registry tree
|
|
tree := mux.getTree(r.method, r.subdomain)
|
|
if tree == nil {
|
|
//first time we register a route to this method with this domain
|
|
tree = &muxTree{method: r.method, subdomain: r.subdomain, entry: &muxEntry{}}
|
|
mux.garden = append(mux.garden, tree)
|
|
}
|
|
// I decide that it's better to explicit give subdomain and a path to it than registedPath(mysubdomain./something) now its: subdomain: mysubdomain., path: /something
|
|
// we have different tree for each of subdomains, now you can use everything you can use with the normal paths ( before you couldn't set /any/*path)
|
|
if err := tree.entry.add(r.path, r.middleware); err != nil {
|
|
mux.logger.Panic(err)
|
|
}
|
|
|
|
if mp := tree.entry.paramsLen; mp > mux.maxParameters {
|
|
mux.maxParameters = mp
|
|
}
|
|
}
|
|
|
|
methodEqual = func(reqMethod string, treeMethod string) bool {
|
|
return reqMethod == treeMethod
|
|
}
|
|
// check for cors conflicts FIRST in order to put them in OPTIONS tree also
|
|
for i := range mux.lookups {
|
|
r := mux.lookups[i]
|
|
if r.hasCors() {
|
|
// cors middleware is updated also, ref: https://github.com/kataras/iris/issues/461
|
|
methodEqual = func(reqMethod string, treeMethod string) bool {
|
|
// preflights
|
|
return reqMethod == MethodOptions || reqMethod == treeMethod
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
func (mux *serveMux) lookup(routeName string) *route {
|
|
for i := range mux.lookups {
|
|
if r := mux.lookups[i]; r.name == routeName {
|
|
return r
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
//THESE ARE FROM Go Authors
|
|
var htmlReplacer = strings.NewReplacer(
|
|
"&", "&",
|
|
"<", "<",
|
|
">", ">",
|
|
// """ is shorter than """.
|
|
`"`, """,
|
|
// "'" is shorter than "'" and apos was not in HTML until HTML5.
|
|
"'", "'",
|
|
)
|
|
|
|
// HTMLEscape returns a string which has no valid html code
|
|
func HTMLEscape(s string) string {
|
|
return htmlReplacer.Replace(s)
|
|
}
|
|
|
|
// BuildHandler the default Iris router when iris.Handler is nil
|
|
func (mux *serveMux) BuildHandler() HandlerFunc {
|
|
|
|
// initialize the router once
|
|
methodEqual := mux.build()
|
|
|
|
return func(context *Context) {
|
|
routePath := context.Path()
|
|
for i := range mux.garden {
|
|
tree := mux.garden[i]
|
|
if !methodEqual(context.Request.Method, tree.method) {
|
|
continue
|
|
}
|
|
|
|
if mux.hosts && tree.subdomain != "" {
|
|
// context.VirtualHost() is a slow method because it makes
|
|
// string.Replaces but user can understand that if subdomain then server will have some nano/or/milleseconds performance cost
|
|
requestHost := context.VirtualHostname()
|
|
if requestHost != mux.hostname {
|
|
//println(requestHost + " != " + mux.hostname)
|
|
// we have a subdomain
|
|
if strings.Index(tree.subdomain, dynamicSubdomainIndicator) != -1 {
|
|
} else {
|
|
//println(requestHost + " = " + mux.hostname)
|
|
// mux.host = iris-go.com:8080, the subdomain for example is api.,
|
|
// so the host must be api.iris-go.com:8080
|
|
if tree.subdomain+mux.hostname != requestHost {
|
|
// go to the next tree, we have a subdomain but it is not the correct
|
|
continue
|
|
}
|
|
|
|
}
|
|
} else {
|
|
//("it's subdomain but the request is the same as the listening addr mux.host == requestHost =>" + mux.host + "=" + requestHost + " ____ and tree's subdomain was: " + tree.subdomain)
|
|
continue
|
|
}
|
|
}
|
|
|
|
mustRedirect := tree.entry.get(routePath, context) // pass the parameters here for 0 allocation
|
|
if context.Middleware != nil {
|
|
// ok we found the correct route, serve it and exit entirely from here
|
|
//ctx.Request.Header.SetUserAgentBytes(DefaultUserAgent)
|
|
context.Do()
|
|
return
|
|
} else if mustRedirect && mux.correctPath { // && context.Method() == MethodConnect {
|
|
reqPath := routePath
|
|
pathLen := len(reqPath)
|
|
|
|
if pathLen > 1 {
|
|
if reqPath[pathLen-1] == '/' {
|
|
reqPath = reqPath[:pathLen-1] //remove the last /
|
|
} else {
|
|
//it has path prefix, it doesn't ends with / and it hasn't be found, then just add the slash
|
|
reqPath = reqPath + "/"
|
|
}
|
|
|
|
urlToRedirect := reqPath
|
|
|
|
statusForRedirect := StatusMovedPermanently // StatusMovedPermanently, this document is obselte, clients caches this.
|
|
if tree.method == MethodPost ||
|
|
tree.method == MethodPut ||
|
|
tree.method == MethodDelete {
|
|
statusForRedirect = StatusTemporaryRedirect // To maintain POST data
|
|
}
|
|
|
|
context.Redirect(urlToRedirect, statusForRedirect)
|
|
// RFC2616 recommends that a short note "SHOULD" be included in the
|
|
// response because older user agents may not understand 301/307.
|
|
// Shouldn't send the response for POST or HEAD; that leaves GET.
|
|
if tree.method == MethodGet {
|
|
note := "<a href=\"" + HTMLEscape(urlToRedirect) + "\">Moved Permanently</a>.\n"
|
|
context.WriteString(note)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
// not found
|
|
break
|
|
}
|
|
// https://github.com/kataras/iris/issues/469
|
|
if mux.fireMethodNotAllowed {
|
|
for i := range mux.garden {
|
|
tree := mux.garden[i]
|
|
if !methodEqual(context.Method(), tree.method) {
|
|
continue
|
|
}
|
|
}
|
|
mux.fireError(StatusMethodNotAllowed, context)
|
|
return
|
|
}
|
|
mux.fireError(StatusNotFound, context)
|
|
}
|
|
}
|
|
|
|
var (
|
|
errPortAlreadyUsed = errors.New("Port is already used")
|
|
errRemoveUnix = errors.New("Unexpected error when trying to remove unix socket file. Addr: %s | Trace: %s")
|
|
errChmod = errors.New("Cannot chmod %#o for %q: %s")
|
|
errCertKeyMissing = errors.New("You should provide certFile and keyFile for TLS/SSL")
|
|
errParseTLS = errors.New("Couldn't load TLS, certFile=%q, keyFile=%q. Trace: %s")
|
|
)
|
|
|
|
// TCP4 returns a new tcp4 Listener
|
|
// *tcp6 has some bugs in some operating systems, as reported by Go Community*
|
|
func TCP4(addr string) (net.Listener, error) {
|
|
return net.Listen("tcp4", ParseHost(addr))
|
|
}
|
|
|
|
// UNIX returns a new unix(file) Listener
|
|
func UNIX(addr string, mode os.FileMode) (net.Listener, error) {
|
|
if errOs := os.Remove(addr); errOs != nil && !os.IsNotExist(errOs) {
|
|
return nil, errRemoveUnix.Format(addr, errOs.Error())
|
|
}
|
|
|
|
listener, err := net.Listen("unix", addr)
|
|
if err != nil {
|
|
return nil, errPortAlreadyUsed.AppendErr(err)
|
|
}
|
|
|
|
if err = os.Chmod(addr, mode); err != nil {
|
|
return nil, errChmod.Format(mode, addr, err.Error())
|
|
}
|
|
|
|
return listener, nil
|
|
}
|
|
|
|
// TLS returns a new TLS Listener
|
|
func TLS(addr, certFile, keyFile string) (net.Listener, error) {
|
|
|
|
if certFile == "" || keyFile == "" {
|
|
return nil, errCertKeyMissing
|
|
}
|
|
|
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
if err != nil {
|
|
return nil, errParseTLS.Format(certFile, keyFile, err)
|
|
}
|
|
|
|
return CERT(addr, cert)
|
|
}
|
|
|
|
// CERT returns a listener which contans tls.Config with the provided certificate, use for ssl
|
|
func CERT(addr string, cert tls.Certificate) (net.Listener, error) {
|
|
ln, err := TCP4(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tlsConfig := &tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
PreferServerCipherSuites: true,
|
|
}
|
|
return tls.NewListener(ln, tlsConfig), nil
|
|
}
|
|
|
|
// LETSENCRYPT returns a new Automatic TLS Listener using letsencrypt.org service
|
|
// receives two parameters, the first is the domain of the server
|
|
// and the second is optionally, the cache file, if you skip it then the cache directory is "./letsencrypt.cache"
|
|
// if you want to disable cache file then simple give it a value of empty string ""
|
|
//
|
|
// supports localhost domains for testing,
|
|
// but I recommend you to use the LETSENCRYPTPROD if you gonna to use it on production
|
|
func LETSENCRYPT(addr string, cacheFileOptional ...string) (net.Listener, error) {
|
|
if portIdx := strings.IndexByte(addr, ':'); portIdx == -1 {
|
|
addr += ":443"
|
|
}
|
|
|
|
ln, err := TCP4(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cacheFile := "./letsencrypt.cache"
|
|
if len(cacheFileOptional) > 0 {
|
|
cacheFile = cacheFileOptional[0]
|
|
}
|
|
|
|
var m letsencrypt.Manager
|
|
|
|
if cacheFile != "" {
|
|
if err = m.CacheFile(cacheFile); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
tlsConfig := &tls.Config{GetCertificate: m.GetCertificate}
|
|
tlsLn := tls.NewListener(ln, tlsConfig)
|
|
|
|
return tlsLn, nil
|
|
}
|
|
|
|
// LETSENCRYPTPROD returns a new Automatic TLS Listener using letsencrypt.org service
|
|
// receives two parameters, the first is the domain of the server
|
|
// and the second is optionally, the cache directory, if you skip it then the cache directory is "./certcache"
|
|
// if you want to disable cache directory then simple give it a value of empty string ""
|
|
//
|
|
// does NOT supports localhost domains for testing, use LETSENCRYPT instead.
|
|
//
|
|
// this is the recommended function to use when you're ready for production state
|
|
func LETSENCRYPTPROD(addr string, cacheDirOptional ...string) (net.Listener, error) {
|
|
if portIdx := strings.IndexByte(addr, ':'); portIdx == -1 {
|
|
addr += ":443"
|
|
}
|
|
|
|
ln, err := TCP4(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cacheDir := "./certcache"
|
|
if len(cacheDirOptional) > 0 {
|
|
cacheDir = cacheDirOptional[0]
|
|
}
|
|
|
|
m := autocert.Manager{
|
|
Prompt: autocert.AcceptTOS,
|
|
} // HostPolicy is missing, if user wants it, then she/he should manually
|
|
// configure the autocertmanager and use the `iris.Serve` to pass that listener
|
|
|
|
if cacheDir == "" {
|
|
// then the user passed empty by own will, then I guess she/he doesnt' want any cache directory
|
|
} else {
|
|
m.Cache = autocert.DirCache(cacheDir)
|
|
}
|
|
|
|
tlsConfig := &tls.Config{GetCertificate: m.GetCertificate}
|
|
tlsLn := tls.NewListener(ln, tlsConfig)
|
|
|
|
return tlsLn, nil
|
|
}
|
|
|
|
// TCPKeepAliveListener sets TCP keep-alive timeouts on accepted
|
|
// connections.
|
|
// Dead TCP connections (e.g. closing laptop mid-download) eventually
|
|
// go away
|
|
// It is not used by default if you want to pass a keep alive listener
|
|
// then just pass the child listener, example:
|
|
// listener := iris.TCPKeepAliveListener{iris.TCP4(":8080").(*net.TCPListener)}
|
|
type TCPKeepAliveListener struct {
|
|
*net.TCPListener
|
|
}
|
|
|
|
// Accept implements the listener and sets the keep alive period which is 2minutes
|
|
func (ln TCPKeepAliveListener) Accept() (c net.Conn, err error) {
|
|
tc, err := ln.AcceptTCP()
|
|
if err != nil {
|
|
return
|
|
}
|
|
tc.SetKeepAlive(true)
|
|
tc.SetKeepAlivePeriod(2 * time.Minute)
|
|
return tc, nil
|
|
}
|
|
|
|
// ParseHost tries to convert a given string to an address which is compatible with net.Listener and server
|
|
func ParseHost(addr string) string {
|
|
// check if addr has :port, if not do it +:80 ,we need the hostname for many cases
|
|
a := addr
|
|
if a == "" {
|
|
// check for os environments
|
|
if oshost := os.Getenv("ADDR"); oshost != "" {
|
|
a = oshost
|
|
} else if oshost := os.Getenv("HOST"); oshost != "" {
|
|
a = oshost
|
|
} else if oshost := os.Getenv("HOSTNAME"); oshost != "" {
|
|
a = oshost
|
|
// check for port also here
|
|
if osport := os.Getenv("PORT"); osport != "" {
|
|
a += ":" + osport
|
|
}
|
|
} else if osport := os.Getenv("PORT"); osport != "" {
|
|
a = ":" + osport
|
|
} else {
|
|
a = DefaultServerAddr
|
|
}
|
|
}
|
|
if portIdx := strings.IndexByte(a, ':'); portIdx == 0 {
|
|
if a[portIdx:] == ":https" {
|
|
a = DefaultServerHostname + ":443"
|
|
} else {
|
|
// if contains only :port ,then the : is the first letter, so we dont have setted a hostname, lets set it
|
|
a = DefaultServerHostname + a
|
|
}
|
|
}
|
|
|
|
/* changed my mind, don't add 80, this will cause problems on unix listeners, and it's not really necessary because we take the port using parsePort
|
|
if portIdx := strings.IndexByte(a, ':'); portIdx < 0 {
|
|
// missing port part, add it
|
|
a = a + ":80"
|
|
}*/
|
|
|
|
return a
|
|
}
|
|
|
|
// ParseHostname receives an addr of form host[:port] and returns the hostname part of it
|
|
// ex: localhost:8080 will return the `localhost`, mydomain.com:8080 will return the 'mydomain'
|
|
func ParseHostname(addr string) string {
|
|
idx := strings.IndexByte(addr, ':')
|
|
if idx == 0 {
|
|
// only port, then return 0.0.0.0
|
|
return "0.0.0.0"
|
|
} else if idx > 0 {
|
|
return addr[0:idx]
|
|
}
|
|
// it's already hostname
|
|
return addr
|
|
}
|
|
|
|
// ParsePort receives an addr of form host[:port] and returns the port part of it
|
|
// ex: localhost:8080 will return the `8080`, mydomain.com will return the '80'
|
|
func ParsePort(addr string) int {
|
|
if portIdx := strings.IndexByte(addr, ':'); portIdx != -1 {
|
|
afP := addr[portIdx+1:]
|
|
p, err := strconv.Atoi(afP)
|
|
if err == nil {
|
|
return p
|
|
} else if afP == "https" { // it's not number, check if it's :https
|
|
return 443
|
|
}
|
|
}
|
|
return 80
|
|
}
|
|
|
|
const (
|
|
// SchemeHTTPS returns "https://" (full)
|
|
SchemeHTTPS = "https://"
|
|
// SchemeHTTP returns "http://" (full)
|
|
SchemeHTTP = "http://"
|
|
)
|
|
|
|
// ParseScheme returns the scheme based on the host,addr,domain
|
|
// Note: the full scheme not just http*,https* *http:// *https://
|
|
func ParseScheme(domain string) string {
|
|
// pure check
|
|
if strings.HasPrefix(domain, SchemeHTTPS) || ParsePort(domain) == 443 {
|
|
return SchemeHTTPS
|
|
}
|
|
return SchemeHTTP
|
|
}
|
|
|
|
// ProxyHandler returns a new net/http.Handler which works as 'proxy', maybe doesn't suits you look its code before using that in production
|
|
var ProxyHandler = func(redirectSchemeAndHost string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// override the handler and redirect all requests to this addr
|
|
redirectTo := redirectSchemeAndHost
|
|
fakehost := r.URL.Host
|
|
path := r.URL.EscapedPath()
|
|
if strings.Count(fakehost, ".") >= 3 { // propably a subdomain, pure check but doesn't matters don't worry
|
|
if sufIdx := strings.LastIndexByte(fakehost, '.'); sufIdx > 0 {
|
|
// check if the last part is a number instead of .com/.gr...
|
|
// if it's number then it's propably is 0.0.0.0 or 127.0.0.1... so it shouldn' use subdomain
|
|
if _, err := strconv.Atoi(fakehost[sufIdx+1:]); err != nil {
|
|
// it's not number then process the try to parse the subdomain
|
|
redirectScheme := ParseScheme(redirectSchemeAndHost)
|
|
realHost := strings.Replace(redirectSchemeAndHost, redirectScheme, "", 1)
|
|
redirectHost := strings.Replace(fakehost, fakehost, realHost, 1)
|
|
redirectTo = redirectScheme + redirectHost + path
|
|
http.Redirect(w, r, redirectTo, StatusMovedPermanently)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if path != "/" {
|
|
redirectTo += path
|
|
}
|
|
if redirectTo == r.URL.String() {
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, redirectTo, StatusMovedPermanently)
|
|
}
|
|
}
|
|
|
|
// Proxy not really a proxy, it's just
|
|
// starts a server listening on proxyAddr but redirects all requests to the redirectToSchemeAndHost+$path
|
|
// nothing special, use it only when you want to start a secondary server which its only work is to redirect from one requested path to another
|
|
//
|
|
// returns a close function
|
|
func Proxy(proxyAddr string, redirectSchemeAndHost string) func() error {
|
|
proxyAddr = ParseHost(proxyAddr)
|
|
|
|
// override the handler and redirect all requests to this addr
|
|
h := ProxyHandler(redirectSchemeAndHost)
|
|
prx := New(OptionDisableBanner(true))
|
|
prx.Router = h
|
|
|
|
go prx.Listen(proxyAddr)
|
|
if ok := <-prx.Available; !ok {
|
|
prx.Logger.Panic("Unexpected error: proxy server cannot start, please report this as bug!!")
|
|
}
|
|
return func() error { return prx.Close() }
|
|
}
|