2017-02-14 04:54:11 +01:00
package httprouter
// +------------------------------------------------------------+
// | Usage |
// +------------------------------------------------------------+
//
//
// package main
//
// import (
// "gopkg.in/kataras/iris.v6/adaptors/httprouter"
// "gopkg.in/kataras/iris.v6"
// )
//
// func main() {
// app := iris.New()
//
// app.Adapt(httprouter.New()) // Add this line and you're ready.
//
// app.Get("/api/users/:userid", func(ctx *iris.Context) {
// ctx.Writef("User with id: %s", ctx.Param("userid"))
// })
//
// app.Listen(":8080")
// }
import (
2017-02-18 07:03:14 +01:00
"fmt"
2017-02-14 04:54:11 +01:00
"net/http"
"strings"
"github.com/kataras/go-errors"
"gopkg.in/kataras/iris.v6"
)
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 iris . Middleware
precedence uint64
paramsLen uint8
}
)
var (
errMuxEntryConflictsWildcard = errors . New ( `
2017-02-15 07:40:43 +01:00
httprouter : ' % s ' in new path ' % s '
2017-02-14 04:54:11 +01:00
conflicts with existing wildcarded route with path : ' % s '
in existing prefix of ' % s ' ` )
errMuxEntryMiddlewareAlreadyExists = errors . New ( `
2017-02-15 07:40:43 +01:00
httprouter : Middleware were already registered for the path : ' % s ' ` )
2017-02-14 04:54:11 +01:00
errMuxEntryInvalidWildcard = errors . New ( `
2017-02-15 07:40:43 +01:00
httprouter : More than one wildcard found in the path part : ' % s ' in route ' s path : ' % s ' ` )
2017-02-14 04:54:11 +01:00
errMuxEntryConflictsExistingWildcard = errors . New ( `
2017-02-15 07:40:43 +01:00
httprouter : Wildcard for route path : ' % s ' conflicts with existing children in route path : ' % s ' ` )
2017-02-14 04:54:11 +01:00
errMuxEntryWildcardUnnamed = errors . New ( `
2017-02-15 07:40:43 +01:00
httprouter : Unnamed wildcard found in path : ' % s ' ` )
2017-02-14 04:54:11 +01:00
errMuxEntryWildcardInvalidPlace = errors . New ( `
2017-02-15 07:40:43 +01:00
httprouter : Wildcard is only allowed at the end of the path , in the route path : ' % s ' ` )
2017-02-14 04:54:11 +01:00
errMuxEntryWildcardConflictsMiddleware = errors . New ( `
2017-02-15 07:40:43 +01:00
httprouter : Wildcard conflicts with existing middleware for the route path : ' % s ' ` )
2017-02-14 04:54:11 +01:00
errMuxEntryWildcardMissingSlash = errors . New ( `
2017-02-15 07:40:43 +01:00
httprouter : No slash ( / ) were found before wildcard in the route path : ' % s ' ` )
2017-02-14 04:54:11 +01:00
)
// 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 iris . 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 ) ] &&
// Check for longer wildcard, e.g. :name and :names
( len ( e . part ) >= len ( path ) || path [ len ( e . part ) ] == '/' ) {
continue loop
} else {
// Wildcard conflict
part := strings . SplitN ( path , "/" , 2 ) [ 0 ]
prefix := fullPath [ : strings . Index ( fullPath , part ) ] + e . part
return errMuxEntryConflictsWildcard . Format ( fullPath , e . part , prefix )
}
}
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
}
return e . addNode ( numParams , path , fullPath , middleware )
} else if i == len ( path ) {
if e . middleware != nil {
return errMuxEntryMiddlewareAlreadyExists . Format ( fullPath )
}
e . middleware = middleware
}
return nil
}
} else {
if err := e . addNode ( numParams , path , fullPath , middleware ) ; err != nil {
return err
}
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 iris . 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 :
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 * iris . 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
}
type (
muxTree struct {
method string
// subdomain is empty for default-hostname routes,
// ex: mysubdomain.
subdomain string
entry * muxEntry
}
serveMux struct {
garden [ ] * muxTree
maxParameters uint8
methodEqual func ( string , string ) bool
hosts bool
}
)
2017-02-18 07:03:14 +01:00
// path = "/api/users/:id"
// return "/api/users/%v"
//
// path = "/files/*file"
// return /files/%v
//
// path = "/:username/messages/:messageid"
// return "/%v/messages/%v"
func formatPath ( path string ) string {
n1Len := strings . Count ( path , ":" )
isMatchEverything := len ( path ) > 0 && path [ len ( path ) - 1 ] == matchEverythingByte
if n1Len == 0 && ! isMatchEverything {
// its a static
return path
}
if n1Len == 0 && isMatchEverything {
//if we have something like: /mypath/anything/* -> /mypatch/anything/%v
return path [ 0 : len ( path ) - 2 ] + "%v"
}
splittedN1 := strings . Split ( path , "/" )
for _ , v := range splittedN1 {
if len ( v ) > 0 {
if v [ 0 ] == ':' || v [ 0 ] == matchEverythingByte {
path = strings . Replace ( path , v , "%v" , - 1 ) // n1Len, but let it we don't care about performance here.
}
}
}
return path
}
2017-02-21 23:51:50 +01:00
// Name is the name of the router
//
// See $iris_instance.Config.Other for more.
const Name = "httprouter"
2017-02-14 04:54:11 +01:00
// New returns a new iris' policy to create and attach the router.
// It's based on the julienschmidt/httprouter with more features and some iris-relative performance tips:
// subdomains(wildcard/dynamic and static) and faster parameters set (use of the already-created context's values)
// and support for reverse routing.
func New ( ) iris . Policies {
var logger func ( iris . LogMode , string )
mux := & serveMux {
methodEqual : func ( reqMethod string , treeMethod string ) bool {
return reqMethod == treeMethod
} ,
}
matchEverythingString := string ( matchEverythingByte )
return iris . Policies {
EventPolicy : iris . EventPolicy {
Boot : func ( s * iris . Framework ) {
logger = s . Log
2017-02-21 23:51:50 +01:00
s . Set ( iris . OptionOther ( iris . RouterNameConfigKey , Name ) )
2017-02-28 04:37:53 +01:00
} } ,
2017-02-14 04:54:11 +01:00
RouterReversionPolicy : iris . RouterReversionPolicy {
// path normalization done on iris' side
StaticPath : func ( path string ) string {
i := strings . IndexByte ( path , parameterStartByte )
2017-03-01 14:04:42 +01:00
v := strings . IndexByte ( path , matchEverythingByte )
if i > - 1 || v > - 1 {
if i < v {
return path [ 0 : i ]
}
// we can't return path[0:0]
if v > 0 {
return path [ 0 : v ]
}
2017-02-14 04:54:11 +01:00
}
return path
} ,
WildcardPath : func ( path string , paramName string ) string {
2017-02-22 15:38:55 +01:00
// *param
wildcardPart := matchEverythingString + paramName
if path [ len ( path ) - 1 ] != slashByte {
// if not ending with slash then prepend the slash to the wildcard path part
wildcardPart = slash + wildcardPart
}
// finally return the path given + the wildcard path part
return path + wildcardPart
2017-02-14 04:54:11 +01:00
} ,
2017-02-28 14:01:18 +01:00
Param : func ( paramName string ) string {
return string ( parameterStartByte ) + paramName
} ,
2017-02-18 07:03:14 +01:00
// path = "/api/users/:id", args = ["42"]
// return "/api/users/42"
2017-02-14 04:54:11 +01:00
//
2017-02-18 07:03:14 +01:00
// path = "/files/*file", args = ["mydir","myfile.zip"]
// return /files/mydir/myfile.zip
2017-02-14 04:54:11 +01:00
//
2017-02-18 07:03:14 +01:00
// path = "/:username/messages/:messageid", args = ["kataras","42"]
// return "/kataras/messages/42"
2017-02-14 04:54:11 +01:00
//
2017-02-18 07:03:14 +01:00
// This policy is used for reverse routing,
// see iris.Path/URL and ~/adaptors/view/ {{ url }} {{ urlpath }}
URLPath : func ( r iris . RouteInfo , args ... string ) string {
rpath := r . Path ( )
formattedPath := formatPath ( rpath )
if rpath == formattedPath {
// static, no need to pass args
return rpath
}
// check if we have /*, if yes then join all arguments to one as path and pass that as parameter
if formattedPath != rpath && rpath [ len ( rpath ) - 1 ] == matchEverythingByte {
parameter := strings . Join ( args , slash )
return fmt . Sprintf ( formattedPath , parameter )
}
// else return the formattedPath with its args
for _ , s := range args {
formattedPath = strings . Replace ( formattedPath , "%v" , s , 1 )
}
return formattedPath
2017-02-18 06:03:37 +01:00
2017-02-18 07:03:14 +01:00
} ,
2017-02-14 04:54:11 +01:00
} ,
RouterBuilderPolicy : func ( repo iris . RouteRepository , context iris . ContextPool ) http . Handler {
fatalErr := false
2017-02-18 06:03:37 +01:00
mux . garden = mux . garden [ 0 : 0 ] // re-set the nodes
mux . hosts = false
2017-02-14 04:54:11 +01:00
repo . Visit ( func ( r iris . RouteInfo ) {
if fatalErr {
return
}
// add to the registry tree
method := r . Method ( )
subdomain := r . Subdomain ( )
path := r . Path ( )
middleware := r . Middleware ( )
tree := mux . getTree ( method , subdomain )
if tree == nil {
//first time we register a route to this method with this domain
tree = & muxTree { method : method , subdomain : 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 registeredPath(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 ( path , middleware ) ; err != nil {
// while ProdMode means that the iris should not continue running
// by-default it panics on these errors, but to make sure let's introduce the fatalErr to stop visiting
fatalErr = true
2017-02-15 07:40:43 +01:00
logger ( iris . ProdMode , err . Error ( ) )
2017-02-14 04:54:11 +01:00
return
}
if mp := tree . entry . paramsLen ; mp > mux . maxParameters {
mux . maxParameters = mp
}
// check for method equality if at least one route has cors
if r . HasCors ( ) {
mux . methodEqual = func ( reqMethod string , treeMethod string ) bool {
// preflights
return reqMethod == iris . MethodOptions || reqMethod == treeMethod
}
}
if subdomain != "" {
mux . hosts = true
}
} )
if ! fatalErr {
return mux . buildHandler ( context )
}
return nil
} ,
}
}
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 ) buildHandler ( pool iris . ContextPool ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
pool . Run ( w , r , func ( context * iris . Context ) {
routePath := context . Path ( )
for i := range mux . garden {
tree := mux . garden [ i ]
if ! mux . methodEqual ( context . Request . Method , tree . method ) {
continue
}
if mux . hosts && tree . subdomain != "" {
2017-02-17 01:14:46 +01:00
requestHost := context . Host ( )
2017-02-14 04:54:11 +01:00
hostname := context . Framework ( ) . Config . VHost
2017-02-17 01:14:46 +01:00
// println("mux are true and tree.subdomain= " + tree.subdomain + "and hostname = " + hostname + " host = " + requestHost)
2017-02-14 04:54:11 +01:00
if requestHost != hostname {
// we have a subdomain
if strings . Contains ( tree . subdomain , iris . DynamicSubdomainIndicator ) {
} else {
if tree . subdomain + hostname != requestHost {
// go to the next tree, we have a subdomain but it is not the correct
continue
}
}
} else {
2017-02-17 01:14:46 +01:00
//("it's subdomain but the request is not the same as the vhost)
2017-02-14 04:54:11 +01:00
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 && ! context . Framework ( ) . Config . DisablePathCorrection { // && 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 := iris . StatusMovedPermanently // StatusMovedPermanently, this document is obselte, clients caches this.
if tree . method == iris . MethodPost ||
tree . method == iris . MethodPut ||
tree . method == iris . MethodDelete {
statusForRedirect = iris . 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 == iris . MethodGet {
note := "<a href=\"" + HTMLEscape ( urlToRedirect ) + "\">Moved Permanently</a>.\n"
// ignore error
context . WriteString ( note )
}
return
}
}
// not found
break
}
// https://github.com/kataras/iris/issues/469
if context . Framework ( ) . Config . FireMethodNotAllowed {
2017-02-21 13:20:31 +01:00
var methodAllowed string
2017-02-14 04:54:11 +01:00
for i := range mux . garden {
tree := mux . garden [ i ]
2017-02-21 13:20:31 +01:00
methodAllowed = tree . method // keep track of the allowed method of the last checked tree
2017-02-14 04:54:11 +01:00
if ! mux . methodEqual ( context . Method ( ) , tree . method ) {
continue
}
}
2017-02-21 13:20:31 +01:00
// RCF rfc2616 https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
// The response MUST include an Allow header containing a list of valid methods for the requested resource.
context . SetHeader ( "Allow" , methodAllowed )
2017-02-14 04:54:11 +01:00
context . EmitError ( iris . StatusMethodNotAllowed )
return
}
context . EmitError ( iris . StatusNotFound )
} )
} )
}
//THESE ARE FROM Go Authors "html" package
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 )
}