mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 18:51:03 +01:00
1420 lines
42 KiB
Go
1420 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) {
|
|
if w, ok := ctx.IsRecording(); ok {
|
|
w.Reset()
|
|
}
|
|
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) {
|
|
if w, ok := ctx.IsRecording(); ok {
|
|
w.Reset()
|
|
}
|
|
ctx.SetStatusCode(statusCode)
|
|
ctx.WriteString(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")
|
|
)
|
|
|
|
// TCPKeepAlive returns a new tcp keep alive Listener
|
|
func TCPKeepAlive(addr string) (net.Listener, error) {
|
|
ln, err := net.Listen("tcp", ParseHost(addr))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return TCPKeepAliveListener{ln.(*net.TCPListener)}, err
|
|
}
|
|
|
|
// TCP4 returns a new tcp4 Listener
|
|
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 3minutes
|
|
func (ln TCPKeepAliveListener) Accept() (c net.Conn, err error) {
|
|
tc, err := ln.AcceptTCP()
|
|
if err != nil {
|
|
return
|
|
}
|
|
tc.SetKeepAlive(true)
|
|
tc.SetKeepAlivePeriod(3 * 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 = ":http"
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
// redirectTo := redirectSchemeAndHost + r.RequestURI
|
|
|
|
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() }
|
|
}
|