mirror of
https://github.com/kataras/iris.git
synced 2026-01-09 13:05:56 +00:00
Publish the new version ✈️ | Look description please!
# FAQ ### Looking for free support? http://support.iris-go.com https://kataras.rocket.chat/channel/iris ### Looking for previous versions? https://github.com/kataras/iris#version ### Should I upgrade my Iris? Developers are not forced to upgrade if they don't really need it. Upgrade whenever you feel ready. > Iris uses the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature, so you get truly reproducible builds, as this method guards against upstream renames and deletes. **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`. For further installation support, please click [here](http://support.iris-go.com/d/16-how-to-install-iris-web-framework). ### About our new home page http://iris-go.com Thanks to [Santosh Anand](https://github.com/santoshanand) the http://iris-go.com has been upgraded and it's really awesome! [Santosh](https://github.com/santoshanand) is a freelancer, he has a great knowledge of nodejs and express js, Android, iOS, React Native, Vue.js etc, if you need a developer to find or create a solution for your problem or task, please contact with him. The amount of the next two or three donations you'll send they will be immediately transferred to his own account balance, so be generous please! Read more at https://github.com/kataras/iris/blob/master/HISTORY.md Former-commit-id: eec2d71bbe011d6b48d2526eb25919e36e5ad94e
This commit is contained in:
659
core/router/api_builder.go
Normal file
659
core/router/api_builder.go
Normal file
@@ -0,0 +1,659 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/errors"
|
||||
"github.com/kataras/iris/core/router/macro"
|
||||
)
|
||||
|
||||
const (
|
||||
// MethodNone is a Virtual method
|
||||
// to store the "offline" routes
|
||||
MethodNone = "NONE"
|
||||
)
|
||||
|
||||
var (
|
||||
// AllMethods contains the valid http methods:
|
||||
// "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD",
|
||||
// "PATCH", "OPTIONS", "TRACE".
|
||||
AllMethods = [...]string{
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"CONNECT",
|
||||
"HEAD",
|
||||
"PATCH",
|
||||
"OPTIONS",
|
||||
"TRACE",
|
||||
}
|
||||
)
|
||||
|
||||
// repository passed to all parties(subrouters), it's the object witch keeps
|
||||
// all the routes.
|
||||
type repository struct {
|
||||
routes []*Route
|
||||
}
|
||||
|
||||
func (r *repository) register(route *Route) {
|
||||
r.routes = append(r.routes, route)
|
||||
}
|
||||
|
||||
func (r *repository) get(routeName string) *Route {
|
||||
for _, r := range r.routes {
|
||||
if r.Name == routeName {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repository) getAll() []*Route {
|
||||
return r.routes
|
||||
}
|
||||
|
||||
type RoutesProvider interface { // api builder
|
||||
GetRoutes() []*Route
|
||||
GetRoute(routeName string) *Route
|
||||
}
|
||||
|
||||
// APIBuilder the visible API for constructing the router
|
||||
// and child routers.
|
||||
type APIBuilder struct {
|
||||
// the api builder global macros registry
|
||||
macros *macro.MacroMap
|
||||
// the api builder global handlers per status code registry (used for custom http errors)
|
||||
errorCodeHandlers *ErrorCodeHandlers
|
||||
// the api builder global routes repository
|
||||
routes *repository
|
||||
// the api builder global route path reverser object
|
||||
// used by the view engine but it can be used anywhere.
|
||||
reverser *RoutePathReverser
|
||||
|
||||
// the per-party middleware
|
||||
middleware context.Handlers
|
||||
// the per-party routes (useful only for done middleware)
|
||||
apiRoutes []*Route
|
||||
// the per-party done middleware
|
||||
doneHandlers context.Handlers
|
||||
// the per-party
|
||||
relativePath string
|
||||
}
|
||||
|
||||
var _ Party = &APIBuilder{}
|
||||
var _ RoutesProvider = &APIBuilder{} // passed to the default request handler (routerHandler)
|
||||
|
||||
// NewAPIBuilder creates & returns a new builder
|
||||
// which is responsible to build the API and the router handler.
|
||||
func NewAPIBuilder() *APIBuilder {
|
||||
rb := &APIBuilder{
|
||||
macros: defaultMacros(),
|
||||
errorCodeHandlers: defaultErrorCodeHandlers(),
|
||||
relativePath: "/",
|
||||
routes: new(repository),
|
||||
}
|
||||
|
||||
return rb
|
||||
}
|
||||
|
||||
// Handle registers a route to the server's rb.
|
||||
// if empty method is passed then handler(s) are being registered to all methods, same as .Any.
|
||||
//
|
||||
// Returns a *Route and an error which will be filled if route wasn't registered successfully.
|
||||
func (rb *APIBuilder) Handle(method string, registeredPath string, handlers ...context.Handler) (*Route, error) {
|
||||
// if registeredPath[0] != '/' {
|
||||
// return nil, errors.New("path should start with slash and should not be empty")
|
||||
// }
|
||||
|
||||
if method == "" || method == "ALL" || method == "ANY" { // then use like it was .Any
|
||||
return nil, rb.Any(registeredPath, handlers...)
|
||||
}
|
||||
|
||||
// no clean path yet because of subdomain indicator/separator which contains a dot.
|
||||
// but remove the first slash if the relative has already ending with a slash
|
||||
// it's not needed because later on we do normalize/clean the path, but better do it here too
|
||||
// for any future updates.
|
||||
if rb.relativePath[len(rb.relativePath)-1] == '/' {
|
||||
if registeredPath[0] == '/' {
|
||||
registeredPath = registeredPath[1:]
|
||||
}
|
||||
}
|
||||
|
||||
fullpath := rb.relativePath + registeredPath // for now, keep the last "/" if any, "/xyz/"
|
||||
|
||||
routeHandlers := joinHandlers(rb.middleware, handlers)
|
||||
|
||||
// here we separate the subdomain and relative path
|
||||
subdomain, path := exctractSubdomain(fullpath)
|
||||
if len(rb.doneHandlers) > 0 {
|
||||
routeHandlers = append(routeHandlers, rb.doneHandlers...) // register the done middleware, if any
|
||||
}
|
||||
|
||||
r, err := NewRoute(method, subdomain, path, routeHandlers, rb.macros)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// global
|
||||
rb.routes.register(r)
|
||||
// per -party
|
||||
rb.apiRoutes = append(rb.apiRoutes, r)
|
||||
// should we remove the rb.apiRoutes on the .Party (new children party) ?, No, because the user maybe use this party later
|
||||
// should we add to the 'inheritance tree' the rb.apiRoutes, No, these are for this specific party only, because the user propably, will have unexpected behavior when using Use/Use, Done/DoneFunc
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Party is just a group joiner of routes which have the same prefix and share same middleware(s) also.
|
||||
// Party could also be named as 'Join' or 'Node' or 'Group' , Party chosen because it is fun.
|
||||
func (rb *APIBuilder) Party(relativePath string, handlers ...context.Handler) Party {
|
||||
parentPath := rb.relativePath
|
||||
dot := string(SubdomainIndicator[0])
|
||||
if len(parentPath) > 0 && parentPath[0] == '/' && strings.HasSuffix(relativePath, dot) { // if ends with . , example: admin., it's subdomain->
|
||||
parentPath = parentPath[1:] // remove first slash
|
||||
}
|
||||
|
||||
fullpath := parentPath + relativePath
|
||||
// append the parent's +child's handlers
|
||||
middleware := joinHandlers(rb.middleware, handlers)
|
||||
|
||||
return &APIBuilder{
|
||||
// global/api builder
|
||||
macros: rb.macros,
|
||||
routes: rb.routes,
|
||||
errorCodeHandlers: rb.errorCodeHandlers,
|
||||
doneHandlers: rb.doneHandlers,
|
||||
// per-party/children
|
||||
middleware: middleware,
|
||||
relativePath: fullpath,
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *APIBuilder) Macros() *macro.MacroMap {
|
||||
return rb.macros
|
||||
}
|
||||
|
||||
// GetRoutes returns the routes information,
|
||||
// some of them can be changed at runtime some others not.
|
||||
//
|
||||
// Needs refresh of the router to Method or Path or Handlers changes to take place.
|
||||
func (rb *APIBuilder) GetRoutes() []*Route {
|
||||
return rb.routes.getAll()
|
||||
}
|
||||
|
||||
// GetRoute returns the registered route based on its name, otherwise nil.
|
||||
// One note: "routeName" should be case-sensitive.
|
||||
func (rb *APIBuilder) GetRoute(routeName string) *Route {
|
||||
return rb.routes.get(routeName)
|
||||
}
|
||||
|
||||
// Use appends Handler(s) to the current Party's routes and child routes.
|
||||
// If the current Party is the root, then it registers the middleware to all child Parties' routes too.
|
||||
func (rb *APIBuilder) Use(handlers ...context.Handler) {
|
||||
rb.middleware = append(rb.middleware, handlers...)
|
||||
}
|
||||
|
||||
// Done appends to the very end, Handler(s) to the current Party's routes and child routes
|
||||
// The difference from .Use is that this/or these Handler(s) are being always running last.
|
||||
func (rb *APIBuilder) Done(handlers ...context.Handler) {
|
||||
if len(rb.apiRoutes) > 0 { // register these middleware on previous-party-defined routes, it called after the party's route methods (Handle/HandleFunc/Get/Post/Put/Delete/...)
|
||||
for i, n := 0, len(rb.apiRoutes); i < n; i++ {
|
||||
routeInfo := rb.apiRoutes[i]
|
||||
routeInfo.Handlers = append(routeInfo.Handlers, handlers...)
|
||||
}
|
||||
} else {
|
||||
// register them on the doneHandlers, which will be used on Handle to append these middlweare as the last handler(s)
|
||||
rb.doneHandlers = append(rb.doneHandlers, handlers...)
|
||||
}
|
||||
}
|
||||
|
||||
// UseGlobal registers Handler middleware to the beginning, prepends them instead of append
|
||||
//
|
||||
// Use it when you want to add a global middleware to all parties, to all routes in all subdomains
|
||||
// It should be called right before Listen functions
|
||||
func (rb *APIBuilder) UseGlobal(handlers ...context.Handler) {
|
||||
for _, r := range rb.routes.routes {
|
||||
r.Handlers = append(handlers, r.Handlers...) // prepend the handlers
|
||||
}
|
||||
rb.middleware = append(handlers, rb.middleware...) // set as middleware on the next routes too
|
||||
// rb.Use(handlers...)
|
||||
}
|
||||
|
||||
// None registers an "offline" route
|
||||
// see context.ExecRoute(routeName) and
|
||||
// party.Routes().Online(handleResultRouteInfo, "GET") and
|
||||
// Offline(handleResultRouteInfo)
|
||||
//
|
||||
// Returns a *Route and an error which will be filled if route wasn't registered successfully.
|
||||
func (rb *APIBuilder) None(path string, handlers ...context.Handler) (*Route, error) {
|
||||
return rb.Handle(MethodNone, path, handlers...)
|
||||
}
|
||||
|
||||
// Get registers a route for the Get http method.
|
||||
//
|
||||
// Returns a *Route and an error which will be filled if route wasn't registered successfully.
|
||||
func (rb *APIBuilder) Get(path string, handlers ...context.Handler) (*Route, error) {
|
||||
return rb.Handle(http.MethodGet, path, handlers...)
|
||||
}
|
||||
|
||||
// Post registers a route for the Post http method.
|
||||
//
|
||||
// Returns a *Route and an error which will be filled if route wasn't registered successfully.
|
||||
func (rb *APIBuilder) Post(path string, handlers ...context.Handler) (*Route, error) {
|
||||
return rb.Handle(http.MethodPost, path, handlers...)
|
||||
}
|
||||
|
||||
// Put registers a route for the Put http method.
|
||||
//
|
||||
// Returns a *Route and an error which will be filled if route wasn't registered successfully.
|
||||
func (rb *APIBuilder) Put(path string, handlers ...context.Handler) (*Route, error) {
|
||||
return rb.Handle(http.MethodPut, path, handlers...)
|
||||
}
|
||||
|
||||
// Delete registers a route for the Delete http method.
|
||||
//
|
||||
// Returns a *Route and an error which will be filled if route wasn't registered successfully.
|
||||
func (rb *APIBuilder) Delete(path string, handlers ...context.Handler) (*Route, error) {
|
||||
return rb.Handle(http.MethodDelete, path, handlers...)
|
||||
}
|
||||
|
||||
// Connect registers a route for the Connect http method.
|
||||
//
|
||||
// Returns a *Route and an error which will be filled if route wasn't registered successfully.
|
||||
func (rb *APIBuilder) Connect(path string, handlers ...context.Handler) (*Route, error) {
|
||||
return rb.Handle(http.MethodConnect, path, handlers...)
|
||||
}
|
||||
|
||||
// Head registers a route for the Head http method.
|
||||
//
|
||||
// Returns a *Route and an error which will be filled if route wasn't registered successfully.
|
||||
func (rb *APIBuilder) Head(path string, handlers ...context.Handler) (*Route, error) {
|
||||
return rb.Handle(http.MethodHead, path, handlers...)
|
||||
}
|
||||
|
||||
// Options registers a route for the Options http method.
|
||||
//
|
||||
// Returns a *Route and an error which will be filled if route wasn't registered successfully.
|
||||
func (rb *APIBuilder) Options(path string, handlers ...context.Handler) (*Route, error) {
|
||||
return rb.Handle(http.MethodOptions, path, handlers...)
|
||||
}
|
||||
|
||||
// Patch registers a route for the Patch http method.
|
||||
//
|
||||
// Returns a *Route and an error which will be filled if route wasn't registered successfully.
|
||||
func (rb *APIBuilder) Patch(path string, handlers ...context.Handler) (*Route, error) {
|
||||
return rb.Handle(http.MethodPatch, path, handlers...)
|
||||
}
|
||||
|
||||
// Trace registers a route for the Trace http method.
|
||||
//
|
||||
// Returns a *Route and an error which will be filled if route wasn't registered successfully.
|
||||
func (rb *APIBuilder) Trace(path string, handlers ...context.Handler) (*Route, error) {
|
||||
return rb.Handle(http.MethodTrace, path, handlers...)
|
||||
}
|
||||
|
||||
// Any registers a route for ALL of the http methods
|
||||
// (Get,Post,Put,Head,Patch,Options,Connect,Delete).
|
||||
func (rb *APIBuilder) Any(registeredPath string, handlers ...context.Handler) error {
|
||||
for _, k := range AllMethods {
|
||||
if _, err := rb.Handle(k, registeredPath, handlers...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StaticCacheDuration expiration duration for INACTIVE file handlers, it's the only one global configuration
|
||||
// which can be changed.
|
||||
var StaticCacheDuration = 20 * time.Second
|
||||
|
||||
const (
|
||||
lastModifiedHeaderKey = "Last-Modified"
|
||||
ifModifiedSinceHeaderKey = "If-Modified-Since"
|
||||
contentDispositionHeaderKey = "Content-Disposition"
|
||||
cacheControlHeaderKey = "Cache-Control"
|
||||
contentEncodingHeaderKey = "Content-Encoding"
|
||||
acceptEncodingHeaderKey = "Accept-Encoding"
|
||||
// contentLengthHeaderKey represents the header["Content-Length"]
|
||||
contentLengthHeaderKey = "Content-Length"
|
||||
contentTypeHeaderKey = "Content-Type"
|
||||
varyHeaderKey = "Vary"
|
||||
)
|
||||
|
||||
func (rb *APIBuilder) registerResourceRoute(reqPath string, h context.Handler) (*Route, error) {
|
||||
if _, err := rb.Head(reqPath, h); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rb.Get(reqPath, h)
|
||||
}
|
||||
|
||||
// StaticHandler returns a new Handler which is ready
|
||||
// to serve all kind of static files.
|
||||
//
|
||||
// Note:
|
||||
// The only difference from package-level `StaticHandler`
|
||||
// is that this `StaticHandler`` receives a request path which
|
||||
// is appended to the party's relative path and stripped here,
|
||||
// so `iris.StripPath` is useless and should not being used here.
|
||||
//
|
||||
// Usage:
|
||||
// app := iris.New()
|
||||
// ...
|
||||
// mySubdomainFsServer := app.Party("mysubdomain.")
|
||||
// h := mySubdomainFsServer.StaticHandler("/static", "./static_files", false, false)
|
||||
// /* http://mysubdomain.mydomain.com/static/css/style.css */
|
||||
// mySubdomainFsServer.Get("/static", h)
|
||||
// ...
|
||||
//
|
||||
func (rb *APIBuilder) StaticHandler(reqPath string, systemPath string, showList bool, enableGzip bool, exceptRoutes ...*Route) context.Handler {
|
||||
return StripPrefix(rb.relativePath+reqPath,
|
||||
StaticHandler(systemPath, showList, enableGzip))
|
||||
}
|
||||
|
||||
// StaticServe serves a directory as web resource
|
||||
// it's the simpliest form of the Static* functions
|
||||
// Almost same usage as StaticWeb
|
||||
// accepts only one required parameter which is the systemPath,
|
||||
// the same path will be used to register the GET and HEAD method routes.
|
||||
// If second parameter is empty, otherwise the requestPath is the second parameter
|
||||
// it uses gzip compression (compression on each request, no file cache).
|
||||
//
|
||||
// Returns the GET *Route.
|
||||
func (rb *APIBuilder) StaticServe(systemPath string, requestPath ...string) (*Route, error) {
|
||||
var reqPath string
|
||||
|
||||
if len(requestPath) == 0 {
|
||||
reqPath = strings.Replace(systemPath, string(os.PathSeparator), "/", -1) // replaces any \ to /
|
||||
reqPath = strings.Replace(reqPath, "//", "/", -1) // for any case, replaces // to /
|
||||
reqPath = strings.Replace(reqPath, ".", "", -1) // replace any dots (./mypath -> /mypath)
|
||||
} else {
|
||||
reqPath = requestPath[0]
|
||||
}
|
||||
|
||||
return rb.Get(joinPath(reqPath, WildcardParam("file")), func(ctx context.Context) {
|
||||
filepath := ctx.Params().Get("file")
|
||||
|
||||
spath := strings.Replace(filepath, "/", string(os.PathSeparator), -1)
|
||||
spath = path.Join(systemPath, spath)
|
||||
|
||||
if !DirectoryExists(spath) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctx.ServeFile(spath, true); err != nil {
|
||||
ctx.StatusCode(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// StaticContent registers a GET and HEAD method routes to the requestPath
|
||||
// that are ready to serve raw static bytes, memory cached.
|
||||
//
|
||||
// Returns the GET *Route.
|
||||
func (rb *APIBuilder) StaticContent(reqPath string, cType string, content []byte) (*Route, error) {
|
||||
modtime := time.Now()
|
||||
h := func(ctx context.Context) {
|
||||
if err := ctx.WriteWithExpiration(http.StatusOK, content, cType, modtime); err != nil {
|
||||
ctx.Application().Log("error while serving []byte via StaticContent: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return rb.registerResourceRoute(reqPath, h)
|
||||
}
|
||||
|
||||
// StaticEmbedded used when files are distributed inside the app executable, using go-bindata mostly
|
||||
// First parameter is the request path, the path which the files in the vdir will be served to, for example "/static"
|
||||
// Second parameter is the (virtual) directory path, for example "./assets"
|
||||
// Third parameter is the Asset function
|
||||
// Forth parameter is the AssetNames function.
|
||||
//
|
||||
// Returns the GET *Route.
|
||||
//
|
||||
// Example: https://github.com/kataras/iris/tree/master/_examples/intermediate/serve-embedded-files
|
||||
func (rb *APIBuilder) StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) (*Route, error) {
|
||||
paramName := "path"
|
||||
|
||||
requestPath = joinPath(requestPath, WildcardParam(paramName))
|
||||
|
||||
if len(vdir) > 0 {
|
||||
if vdir[0] == '.' { // first check for .wrong
|
||||
vdir = vdir[1:]
|
||||
}
|
||||
if vdir[0] == '/' || vdir[0] == os.PathSeparator { // second check for /something, (or ./something if we had dot on 0 it will be removed
|
||||
vdir = vdir[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// collect the names we are care for, because not all Asset used here, we need the vdir's assets.
|
||||
allNames := namesFn()
|
||||
|
||||
var names []string
|
||||
for _, path := range allNames {
|
||||
// check if path is the path name we care for
|
||||
if !strings.HasPrefix(path, vdir) {
|
||||
continue
|
||||
}
|
||||
names = append(names, cleanPath(path))
|
||||
// path = strings.Replace(path, "\\", "/", -1) // replace system paths with double slashes
|
||||
// path = strings.Replace(path, "./", "/", -1) // replace ./assets/favicon.ico to /assets/favicon.ico in order to be ready for compare with the reqPath later
|
||||
// path = path[len(vdir):] // set it as the its 'relative' ( we should re-setted it when assetFn will be used)
|
||||
// names = append(names, path)
|
||||
}
|
||||
|
||||
if len(names) == 0 {
|
||||
return nil, errors.New("unable to locate any embedded files located to the (virtual) directory: " + vdir)
|
||||
}
|
||||
|
||||
modtime := time.Now()
|
||||
h := func(ctx context.Context) {
|
||||
reqPath := ctx.Params().Get(paramName)
|
||||
for _, path := range names {
|
||||
// in order to map "/" as "/index.html"
|
||||
// as requested here: https://github.com/kataras/iris/issues/633#issuecomment-281691851
|
||||
if path == "/index.html" {
|
||||
if reqPath[len(reqPath)-1] == '/' {
|
||||
reqPath = "/index.html"
|
||||
}
|
||||
}
|
||||
|
||||
if path != reqPath {
|
||||
continue
|
||||
}
|
||||
|
||||
cType := TypeByExtension(path)
|
||||
fullpath := vdir + path
|
||||
|
||||
buf, err := assetFn(fullpath)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ctx.WriteWithExpiration(http.StatusOK, buf, cType, modtime); err != nil {
|
||||
ctx.StatusCode(http.StatusInternalServerError)
|
||||
ctx.StopExecution()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// not found or error
|
||||
ctx.NotFound()
|
||||
|
||||
}
|
||||
|
||||
return rb.registerResourceRoute(requestPath, h)
|
||||
}
|
||||
|
||||
// errDirectoryFileNotFound returns an error with message: 'Directory or file %s couldn't found. Trace: +error trace'
|
||||
var errDirectoryFileNotFound = errors.New("Directory or file %s couldn't found. Trace: %s")
|
||||
|
||||
// Favicon serves static favicon
|
||||
// accepts 2 parameters, second is optional
|
||||
// favPath (string), declare the system directory path of the __.ico
|
||||
// requestPath (string), it's the route's path, by default this is the "/favicon.ico" because some browsers tries to get this by default first,
|
||||
// you can declare your own path if you have more than one favicon (desktop, mobile and so on)
|
||||
//
|
||||
// this func will add a route for you which will static serve the /yuorpath/yourfile.ico to the /yourfile.ico
|
||||
// (nothing special that you can't handle by yourself).
|
||||
// Note that you have to call it on every favicon you have to serve automatically (desktop, mobile and so on).
|
||||
//
|
||||
// Returns the GET *Route.
|
||||
func (rb *APIBuilder) Favicon(favPath string, requestPath ...string) (*Route, error) {
|
||||
favPath = Abs(favPath)
|
||||
|
||||
f, err := os.Open(favPath)
|
||||
if err != nil {
|
||||
return nil, errDirectoryFileNotFound.Format(favPath, err.Error())
|
||||
}
|
||||
|
||||
// ignore error f.Close()
|
||||
defer f.Close()
|
||||
fi, _ := f.Stat()
|
||||
if fi.IsDir() { // if it's dir the try to get the favicon.ico
|
||||
fav := path.Join(favPath, "favicon.ico")
|
||||
f, err = os.Open(fav)
|
||||
if err != nil {
|
||||
//we try again with .png
|
||||
return rb.Favicon(path.Join(favPath, "favicon.png"))
|
||||
}
|
||||
favPath = fav
|
||||
fi, _ = f.Stat()
|
||||
}
|
||||
|
||||
cType := TypeByFilename(favPath)
|
||||
// copy the bytes here in order to cache and not read the ico on each request.
|
||||
cacheFav := make([]byte, fi.Size())
|
||||
if _, err = f.Read(cacheFav); err != nil {
|
||||
// Here we are before actually run the server.
|
||||
// So we could panic but we don't,
|
||||
// we just interrupt with a message
|
||||
// to the (user-defined) logger.
|
||||
return nil, errDirectoryFileNotFound.
|
||||
Format(favPath, "favicon: couldn't read the data bytes for file: "+err.Error())
|
||||
}
|
||||
modtime := ""
|
||||
h := func(ctx context.Context) {
|
||||
if modtime == "" {
|
||||
modtime = fi.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
|
||||
}
|
||||
if t, err := time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), ctx.GetHeader(ifModifiedSinceHeaderKey)); err == nil && fi.ModTime().Before(t.Add(StaticCacheDuration)) {
|
||||
|
||||
ctx.ResponseWriter().Header().Del(contentTypeHeaderKey)
|
||||
ctx.ResponseWriter().Header().Del(contentLengthHeaderKey)
|
||||
ctx.StatusCode(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ResponseWriter().Header().Set(contentTypeHeaderKey, cType)
|
||||
ctx.ResponseWriter().Header().Set(lastModifiedHeaderKey, modtime)
|
||||
ctx.StatusCode(http.StatusOK)
|
||||
if _, err := ctx.Write(cacheFav); err != nil {
|
||||
ctx.Application().Log("error while trying to serve the favicon: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
reqPath := "/favicon" + path.Ext(fi.Name()) //we could use the filename, but because standards is /favicon.ico/.png.
|
||||
if len(requestPath) > 0 && requestPath[0] != "" {
|
||||
reqPath = requestPath[0]
|
||||
}
|
||||
|
||||
return rb.registerResourceRoute(reqPath, h)
|
||||
}
|
||||
|
||||
// StaticWeb returns a handler that serves HTTP requests
|
||||
// with the contents of the file system rooted at directory.
|
||||
//
|
||||
// first parameter: the route path
|
||||
// second parameter: the system directory
|
||||
// third OPTIONAL parameter: the exception routes
|
||||
// (= give priority to these routes instead of the static handler)
|
||||
// for more options look rb.StaticHandler.
|
||||
//
|
||||
// rb.StaticWeb("/static", "./static")
|
||||
//
|
||||
// As a special case, the returned file server redirects any request
|
||||
// ending in "/index.html" to the same path, without the final
|
||||
// "index.html".
|
||||
//
|
||||
// StaticWeb calls the StaticHandler(requestPath, systemPath, listingDirectories: false, gzip: false ).
|
||||
//
|
||||
// Returns the GET *Route.
|
||||
func (rb *APIBuilder) StaticWeb(reqPath string, systemPath string, exceptRoutes ...*Route) (*Route, error) {
|
||||
h := rb.StaticHandler(reqPath, systemPath, false, false, exceptRoutes...)
|
||||
paramName := "file"
|
||||
routePath := joinPath(reqPath, WildcardParam(paramName))
|
||||
handler := func(ctx context.Context) {
|
||||
h(ctx)
|
||||
// re-check the content type here for any case,
|
||||
// although the new code does it automatically but it's good to have it here.
|
||||
if ctx.GetStatusCode() >= 200 && ctx.GetStatusCode() < 400 {
|
||||
if fname := ctx.Params().Get(paramName); fname != "" {
|
||||
cType := TypeByFilename(fname)
|
||||
ctx.ContentType(cType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rb.registerResourceRoute(routePath, handler)
|
||||
}
|
||||
|
||||
// OnErrorCode registers an error http status code
|
||||
// based on the "statusCode" >= 400.
|
||||
// The handler is being wrapepd by a generic
|
||||
// handler which will try to reset
|
||||
// the body if recorder was enabled
|
||||
// and/or disable the gzip if gzip response recorder
|
||||
// was active.
|
||||
func (rb *APIBuilder) OnErrorCode(statusCode int, handler context.Handler) {
|
||||
rb.errorCodeHandlers.Register(statusCode, handler)
|
||||
}
|
||||
|
||||
// FireErrorCode executes an error http status code handler
|
||||
// based on the context's status code.
|
||||
//
|
||||
// If a handler is not already registered,
|
||||
// then it creates & registers a new trivial handler on the-fly.
|
||||
func (rb *APIBuilder) FireErrorCode(ctx context.Context) {
|
||||
rb.errorCodeHandlers.Fire(ctx)
|
||||
}
|
||||
|
||||
// Layout oerrides the parent template layout with a more specific layout for this Party
|
||||
// returns this Party, to continue as normal
|
||||
// Usage:
|
||||
// app := iris.New()
|
||||
// my := app.Party("/my").Layout("layouts/mylayout.html")
|
||||
// {
|
||||
// my.Get("/", func(ctx context.Context) {
|
||||
// ctx.MustRender("page1.html", nil)
|
||||
// })
|
||||
// }
|
||||
func (rb *APIBuilder) Layout(tmplLayoutFile string) Party {
|
||||
rb.Use(func(ctx context.Context) {
|
||||
ctx.ViewLayout(tmplLayoutFile)
|
||||
ctx.Next()
|
||||
})
|
||||
|
||||
return rb
|
||||
}
|
||||
|
||||
// joinHandlers uses to create a copy of all Handlers and return them in order to use inside the node
|
||||
func joinHandlers(Handlers1 context.Handlers, Handlers2 context.Handlers) context.Handlers {
|
||||
nowLen := len(Handlers1)
|
||||
totalLen := nowLen + len(Handlers2)
|
||||
// create a new slice of Handlers in order to store all handlers, the already handlers(Handlers) and the new
|
||||
newHandlers := make(context.Handlers, totalLen)
|
||||
//copy the already Handlers to the just created
|
||||
copy(newHandlers, Handlers1)
|
||||
//start from there we finish, and store the new Handlers too
|
||||
copy(newHandlers[nowLen:], Handlers2)
|
||||
return newHandlers
|
||||
}
|
||||
977
core/router/fs.go
Normal file
977
core/router/fs.go
Normal file
@@ -0,0 +1,977 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ & Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/errors"
|
||||
)
|
||||
|
||||
// Prioritize is a middleware which executes a route against this path
|
||||
// when the request's Path has a prefix of the route's STATIC PART
|
||||
// is not executing ExecRoute to determinate if it's valid, for performance reasons
|
||||
// if this function is not enough for you and you want to test more than one parameterized path
|
||||
// then use the: if c := ExecRoute(r); c == nil { /* move to the next, the route is not valid */ }
|
||||
//
|
||||
// You can find the Route by iris.Default.Routes().Lookup("theRouteName")
|
||||
// you can set a route name as: myRoute := iris.Default.Get("/mypath", handler)("theRouteName")
|
||||
// that will set a name to the route and returns its iris.Route instance for further usage.
|
||||
//
|
||||
// if the route found then it executes that and don't continue to the next handler
|
||||
// if not found then continue to the next handler
|
||||
func Prioritize(r *Route) context.Handler {
|
||||
if r != nil {
|
||||
return func(ctx context.Context) {
|
||||
reqPath := ctx.Path()
|
||||
staticPath := ResolveStaticPath(reqPath)
|
||||
if strings.HasPrefix(reqPath, staticPath) {
|
||||
ctx.Exec(r.Method, reqPath) // execute the route based on this request path
|
||||
// we are done here.
|
||||
return
|
||||
}
|
||||
// execute the next handler if no prefix
|
||||
// here look, the only error we catch is the 404.
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
return func(ctx context.Context) { ctx.Next() }
|
||||
}
|
||||
|
||||
// StaticHandler returns a new Handler which is ready
|
||||
// to serve all kind of static files.
|
||||
//
|
||||
// Developers can wrap this handler using the `iris.StripPrefix`
|
||||
// for a fixed static path when the result handler is being, finally, registered to a route.
|
||||
//
|
||||
//
|
||||
// Usage:
|
||||
// app := iris.New()
|
||||
// ...
|
||||
// fileserver := iris.StaticHandler("./static_files", false, false)
|
||||
// h := iris.StripPrefix("/static", fileserver)
|
||||
// /* http://mydomain.com/static/css/style.css */
|
||||
// app.Get("/static", h)
|
||||
// ...
|
||||
//
|
||||
func StaticHandler(systemPath string, showList bool, enableGzip bool, exceptRoutes ...*Route) context.Handler {
|
||||
return NewStaticHandlerBuilder(systemPath).
|
||||
Listing(showList).
|
||||
Gzip(enableGzip).
|
||||
Except(exceptRoutes...).
|
||||
Build()
|
||||
}
|
||||
|
||||
// StaticHandlerBuilder is the web file system's Handler builder
|
||||
// use that or the iris.StaticHandler/StaticWeb methods
|
||||
type StaticHandlerBuilder interface {
|
||||
Gzip(enable bool) StaticHandlerBuilder
|
||||
Listing(listDirectoriesOnOff bool) StaticHandlerBuilder
|
||||
Except(r ...*Route) StaticHandlerBuilder
|
||||
Build() context.Handler
|
||||
}
|
||||
|
||||
// +------------------------------------------------------------+
|
||||
// | |
|
||||
// | Static Builder |
|
||||
// | |
|
||||
// +------------------------------------------------------------+
|
||||
|
||||
type fsHandler struct {
|
||||
// user options, only directory is required.
|
||||
directory http.Dir
|
||||
gzip bool
|
||||
listDirectories bool
|
||||
// these are init on the Build() call
|
||||
filesystem http.FileSystem
|
||||
once sync.Once
|
||||
exceptions []*Route
|
||||
handler context.Handler
|
||||
}
|
||||
|
||||
func toWebPath(systemPath string) string {
|
||||
// winos slash to slash
|
||||
webpath := strings.Replace(systemPath, "\\", "/", -1)
|
||||
// double slashes to single
|
||||
webpath = strings.Replace(webpath, "//", "/", -1)
|
||||
// remove all dots
|
||||
webpath = strings.Replace(webpath, ".", "", -1)
|
||||
return webpath
|
||||
}
|
||||
|
||||
// Abs calls filepath.Abs but ignores the error and
|
||||
// returns the original value if any error occurred.
|
||||
func Abs(path string) string {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
return absPath
|
||||
}
|
||||
|
||||
// NewStaticHandlerBuilder returns a new Handler which serves static files
|
||||
// supports gzip, no listing and much more
|
||||
// Note that, this static builder returns a Handler
|
||||
// it doesn't cares about the rest of your iris configuration.
|
||||
//
|
||||
// Use the iris.StaticHandler/StaticWeb in order to serve static files on more automatic way
|
||||
// this builder is used by people who have more complicated application
|
||||
// structure and want a fluent api to work on.
|
||||
func NewStaticHandlerBuilder(dir string) StaticHandlerBuilder {
|
||||
return &fsHandler{
|
||||
directory: http.Dir(Abs(dir)),
|
||||
// gzip is disabled by default
|
||||
gzip: false,
|
||||
// list directories disabled by default
|
||||
listDirectories: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Gzip if enable is true then gzip compression is enabled for this static directory
|
||||
// Defaults to false
|
||||
func (w *fsHandler) Gzip(enable bool) StaticHandlerBuilder {
|
||||
w.gzip = enable
|
||||
return w
|
||||
}
|
||||
|
||||
// Listing turn on/off the 'show files and directories'.
|
||||
// Defaults to false
|
||||
func (w *fsHandler) Listing(listDirectoriesOnOff bool) StaticHandlerBuilder {
|
||||
w.listDirectories = listDirectoriesOnOff
|
||||
return w
|
||||
}
|
||||
|
||||
// Except add a route exception,
|
||||
// gives priority to that Route over the static handler.
|
||||
func (w *fsHandler) Except(r ...*Route) StaticHandlerBuilder {
|
||||
w.exceptions = append(w.exceptions, r...)
|
||||
return w
|
||||
}
|
||||
|
||||
type (
|
||||
noListFile struct {
|
||||
http.File
|
||||
}
|
||||
)
|
||||
|
||||
// Overrides the Readdir of the http.File in order to disable showing a list of the dirs/files
|
||||
func (n noListFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Implements the http.Filesystem
|
||||
// Do not call it.
|
||||
func (w *fsHandler) Open(name string) (http.File, error) {
|
||||
info, err := w.filesystem.Open(name)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !w.listDirectories {
|
||||
return noListFile{info}, nil
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// Build the handler (once) and returns it
|
||||
func (w *fsHandler) Build() context.Handler {
|
||||
// we have to ensure that Build is called ONLY one time,
|
||||
// one instance per one static directory.
|
||||
w.once.Do(func() {
|
||||
w.filesystem = w.directory
|
||||
|
||||
fileserver := func(ctx context.Context) {
|
||||
upath := ctx.Request().URL.Path
|
||||
if !strings.HasPrefix(upath, "/") {
|
||||
upath = "/" + upath
|
||||
ctx.Request().URL.Path = upath
|
||||
}
|
||||
|
||||
// Note the request.url.path is changed but request.RequestURI is not
|
||||
// so on custom errors we use the requesturi instead.
|
||||
// this can be changed
|
||||
_, prevStatusCode := serveFile(ctx,
|
||||
w.filesystem,
|
||||
path.Clean(upath),
|
||||
false,
|
||||
w.listDirectories,
|
||||
(w.gzip && ctx.ClientSupportsGzip()),
|
||||
)
|
||||
|
||||
// check for any http errors after the file handler executed
|
||||
if prevStatusCode >= 400 { // error found (404 or 400 or 500 usually)
|
||||
if writer, ok := ctx.ResponseWriter().(*context.GzipResponseWriter); ok && writer != nil {
|
||||
writer.ResetBody()
|
||||
writer.Disable()
|
||||
// ctx.ResponseWriter.Header().Del(contentType) // application/x-gzip sometimes lawl
|
||||
// remove gzip headers
|
||||
// headers := ctx.ResponseWriter.Header()
|
||||
// headers[contentType] = nil
|
||||
// headers["X-Content-Type-Options"] = nil
|
||||
// headers[varyHeader] = nil
|
||||
// headers[contentEncodingHeader] = nil
|
||||
// headers[contentLength] = nil
|
||||
}
|
||||
ctx.StatusCode(prevStatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// go to the next middleware
|
||||
ctx.Next()
|
||||
}
|
||||
|
||||
if len(w.exceptions) > 0 {
|
||||
middleware := make(context.Handlers, len(w.exceptions)+1)
|
||||
for i := range w.exceptions {
|
||||
middleware[i] = Prioritize(w.exceptions[i])
|
||||
}
|
||||
middleware[len(w.exceptions)] = fileserver
|
||||
|
||||
w.handler = func(ctx context.Context) {
|
||||
ctxHandlers := ctx.Handlers()
|
||||
ctx.SetHandlers(append(middleware, ctxHandlers...))
|
||||
ctx.Handlers()[0](ctx)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.handler = fileserver
|
||||
|
||||
})
|
||||
|
||||
return w.handler
|
||||
}
|
||||
|
||||
// StripPrefix returns a handler that serves HTTP requests
|
||||
// by removing the given prefix from the request URL's Path
|
||||
// and invoking the handler h. StripPrefix handles a
|
||||
// request for a path that doesn't begin with prefix by
|
||||
// replying with an HTTP 404 not found error.
|
||||
//
|
||||
// Usage:
|
||||
// fileserver := iris.StaticHandler("./static_files", false, false)
|
||||
// h := iris.StripPrefix("/static", fileserver)
|
||||
// app.Get("/static", h)
|
||||
//
|
||||
func StripPrefix(prefix string, h context.Handler) context.Handler {
|
||||
if prefix == "" {
|
||||
return h
|
||||
}
|
||||
// here we separate the path from the subdomain (if any), we care only for the path
|
||||
// fixes a bug when serving static files via a subdomain
|
||||
fixedPrefix := prefix
|
||||
if dotWSlashIdx := strings.Index(fixedPrefix, SubdomainIndicator); dotWSlashIdx > 0 {
|
||||
fixedPrefix = fixedPrefix[dotWSlashIdx+1:]
|
||||
}
|
||||
fixedPrefix = toWebPath(fixedPrefix)
|
||||
|
||||
return func(ctx context.Context) {
|
||||
if p := strings.TrimPrefix(ctx.Request().URL.Path, fixedPrefix); len(p) < len(ctx.Request().URL.Path) {
|
||||
ctx.Request().URL.Path = p
|
||||
h(ctx)
|
||||
} else {
|
||||
ctx.NotFound()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// +------------------------------------------------------------+
|
||||
// | |
|
||||
// | serve file handler |
|
||||
// | edited from net/http/fs.go in order to support GZIP with |
|
||||
// | custom iris http errors and fallback to non-compressed data|
|
||||
// | when not supported. |
|
||||
// | |
|
||||
// +------------------------------------------------------------+
|
||||
|
||||
var htmlReplacer = strings.NewReplacer(
|
||||
"&", "&",
|
||||
"<", "<",
|
||||
">", ">",
|
||||
// """ is shorter than """.
|
||||
`"`, """,
|
||||
// "'" is shorter than "'" and apos was not in HTML until HTML5.
|
||||
"'", "'",
|
||||
)
|
||||
|
||||
func dirList(ctx context.Context, f http.File) (string, int) {
|
||||
dirs, err := f.Readdir(-1)
|
||||
if err != nil {
|
||||
// TODO: log err.Error() to the Server.ErrorLog, once it's possible
|
||||
// for a handler to get at its Server via the http.ResponseWriter. See
|
||||
// Issue 12438.
|
||||
return "Error reading directory", http.StatusInternalServerError
|
||||
|
||||
}
|
||||
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
|
||||
ctx.ContentType("text/html")
|
||||
fmt.Fprintf(ctx.ResponseWriter(), "<pre>\n")
|
||||
for _, d := range dirs {
|
||||
name := d.Name()
|
||||
if d.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
// name may contain '?' or '#', which must be escaped to remain
|
||||
// part of the URL path, and not indicate the start of a query
|
||||
// string or fragment.
|
||||
url := url.URL{Path: name}
|
||||
fmt.Fprintf(ctx.ResponseWriter(), "<a href=\"%s\">%s</a>\n", url.String(), htmlReplacer.Replace(name))
|
||||
}
|
||||
fmt.Fprintf(ctx.ResponseWriter(), "</pre>\n")
|
||||
return "", http.StatusOK
|
||||
}
|
||||
|
||||
// errSeeker is returned by ServeContent's sizeFunc when the content
|
||||
// doesn't seek properly. The underlying Seeker's error text isn't
|
||||
// included in the sizeFunc reply so it's not sent over HTTP to end
|
||||
// users.
|
||||
var errSeeker = errors.New("seeker can't seek")
|
||||
|
||||
// errNoOverlap is returned by serveContent's parseRange if first-byte-pos of
|
||||
// all of the byte-range-spec values is greater than the content size.
|
||||
var errNoOverlap = errors.New("invalid range: failed to overlap")
|
||||
|
||||
// The algorithm uses at most sniffLen bytes to make its decision.
|
||||
const sniffLen = 512
|
||||
|
||||
// if name is empty, filename is unknown. (used for mime type, before sniffing)
|
||||
// if modtime.IsZero(), modtime is unknown.
|
||||
// content must be seeked to the beginning of the file.
|
||||
// The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.
|
||||
func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker, gzip bool) (string, int) /* we could use the TransactionErrResult but prefer not to create new objects for each of the errors on static file handlers*/ {
|
||||
|
||||
setLastModified(ctx, modtime)
|
||||
done, rangeReq := checkPreconditions(ctx, modtime)
|
||||
if done {
|
||||
return "", http.StatusNotModified
|
||||
}
|
||||
|
||||
code := http.StatusOK
|
||||
|
||||
// If Content-Type isn't set, use the file's extension to find it, but
|
||||
// if the Content-Type is unset explicitly, do not sniff the type.
|
||||
ctypes, haveType := ctx.ResponseWriter().Header()["Content-Type"]
|
||||
var ctype string
|
||||
|
||||
if !haveType {
|
||||
ctype = TypeByExtension(filepath.Ext(name))
|
||||
if ctype == "" {
|
||||
|
||||
// read a chunk to decide between utf-8 text and binary
|
||||
var buf [sniffLen]byte
|
||||
n, _ := io.ReadFull(content, buf[:])
|
||||
ctype = http.DetectContentType(buf[:n])
|
||||
_, err := content.Seek(0, io.SeekStart) // rewind to output whole file
|
||||
if err != nil {
|
||||
return "seeker can't seek", http.StatusInternalServerError
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ctx.ContentType(ctype)
|
||||
} else if len(ctypes) > 0 {
|
||||
ctype = ctypes[0]
|
||||
}
|
||||
|
||||
size, err := sizeFunc()
|
||||
if err != nil {
|
||||
return err.Error(), http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// handle Content-Range header.
|
||||
sendSize := size
|
||||
var sendContent io.Reader = content
|
||||
|
||||
if gzip {
|
||||
_ = ctx.GzipResponseWriter()
|
||||
}
|
||||
if size >= 0 {
|
||||
ranges, err := parseRange(rangeReq, size)
|
||||
if err != nil {
|
||||
if err == errNoOverlap {
|
||||
ctx.Header("Content-Range", fmt.Sprintf("bytes */%d", size))
|
||||
}
|
||||
return err.Error(), http.StatusRequestedRangeNotSatisfiable
|
||||
|
||||
}
|
||||
if sumRangesSize(ranges) > size {
|
||||
// The total number of bytes in all the ranges
|
||||
// is larger than the size of the file by
|
||||
// itself, so this is probably an attack, or a
|
||||
// dumb client. Ignore the range request.
|
||||
ranges = nil
|
||||
}
|
||||
switch {
|
||||
case len(ranges) == 1:
|
||||
// RFC 2616, Section 14.16:
|
||||
// "When an HTTP message includes the content of a single
|
||||
// range (for example, a response to a request for a
|
||||
// single range, or to a request for a set of ranges
|
||||
// that overlap without any holes), this content is
|
||||
// transmitted with a Content-Range header, and a
|
||||
// Content-Length header showing the number of bytes
|
||||
// actually transferred.
|
||||
// ...
|
||||
// A response to a request for a single range MUST NOT
|
||||
// be sent using the multipart/byteranges media type."
|
||||
ra := ranges[0]
|
||||
if _, err := content.Seek(ra.start, io.SeekStart); err != nil {
|
||||
return err.Error(), http.StatusRequestedRangeNotSatisfiable
|
||||
}
|
||||
sendSize = ra.length
|
||||
code = http.StatusPartialContent
|
||||
ctx.Header("Content-Range", ra.contentRange(size))
|
||||
case len(ranges) > 1:
|
||||
sendSize = rangesMIMESize(ranges, ctype, size)
|
||||
code = http.StatusPartialContent
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
mw := multipart.NewWriter(pw)
|
||||
ctx.ContentType("multipart/byteranges; boundary=" + mw.Boundary())
|
||||
sendContent = pr
|
||||
defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
|
||||
go func() {
|
||||
for _, ra := range ranges {
|
||||
part, err := mw.CreatePart(ra.mimeHeader(ctype, size))
|
||||
if err != nil {
|
||||
pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
if _, err := content.Seek(ra.start, io.SeekStart); err != nil {
|
||||
pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
if _, err := io.CopyN(part, content, ra.length); err != nil {
|
||||
pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
mw.Close()
|
||||
pw.Close()
|
||||
}()
|
||||
}
|
||||
ctx.Header("Accept-Ranges", "bytes")
|
||||
if ctx.ResponseWriter().Header().Get(contentEncodingHeaderKey) == "" {
|
||||
|
||||
ctx.Header(contentLengthHeaderKey, strconv.FormatInt(sendSize, 10))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.StatusCode(code)
|
||||
|
||||
if ctx.Method() != http.MethodHead {
|
||||
io.CopyN(ctx.ResponseWriter(), sendContent, sendSize)
|
||||
}
|
||||
|
||||
return "", code
|
||||
}
|
||||
|
||||
// scanETag determines if a syntactically valid ETag is present at s. If so,
|
||||
// the ETag and remaining text after consuming ETag is returned. Otherwise,
|
||||
// it returns "", "".
|
||||
func scanETag(s string) (etag string, remain string) {
|
||||
s = textproto.TrimString(s)
|
||||
start := 0
|
||||
if strings.HasPrefix(s, "W/") {
|
||||
start = 2
|
||||
}
|
||||
if len(s[start:]) < 2 || s[start] != '"' {
|
||||
return "", ""
|
||||
}
|
||||
// ETag is either W/"text" or "text".
|
||||
// See RFC 7232 2.3.
|
||||
for i := start + 1; i < len(s); i++ {
|
||||
c := s[i]
|
||||
switch {
|
||||
// Character values allowed in ETags.
|
||||
case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
|
||||
case c == '"':
|
||||
return string(s[:i+1]), s[i+1:]
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// etagStrongMatch reports whether a and b match using strong ETag comparison.
|
||||
// Assumes a and b are valid ETags.
|
||||
func etagStrongMatch(a, b string) bool {
|
||||
return a == b && a != "" && a[0] == '"'
|
||||
}
|
||||
|
||||
// etagWeakMatch reports whether a and b match using weak ETag comparison.
|
||||
// Assumes a and b are valid ETags.
|
||||
func etagWeakMatch(a, b string) bool {
|
||||
return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/")
|
||||
}
|
||||
|
||||
// condResult is the result of an HTTP request precondition check.
|
||||
// See https://tools.ietf.org/html/rfc7232 section 3.
|
||||
type condResult int
|
||||
|
||||
const (
|
||||
condNone condResult = iota
|
||||
condTrue
|
||||
condFalse
|
||||
)
|
||||
|
||||
func checkIfMatch(ctx context.Context) condResult {
|
||||
im := ctx.GetHeader("If-Match")
|
||||
if im == "" {
|
||||
return condNone
|
||||
}
|
||||
for {
|
||||
im = textproto.TrimString(im)
|
||||
if len(im) == 0 {
|
||||
break
|
||||
}
|
||||
if im[0] == ',' {
|
||||
im = im[1:]
|
||||
continue
|
||||
}
|
||||
if im[0] == '*' {
|
||||
return condTrue
|
||||
}
|
||||
etag, remain := scanETag(im)
|
||||
if etag == "" {
|
||||
break
|
||||
}
|
||||
if etagStrongMatch(etag, ctx.ResponseWriter().Header().Get("Etag")) {
|
||||
return condTrue
|
||||
}
|
||||
im = remain
|
||||
}
|
||||
|
||||
return condFalse
|
||||
}
|
||||
|
||||
func checkIfUnmodifiedSince(ctx context.Context, modtime time.Time) condResult {
|
||||
ius := ctx.GetHeader("If-Unmodified-Since")
|
||||
if ius == "" || isZeroTime(modtime) {
|
||||
return condNone
|
||||
}
|
||||
if t, err := http.ParseTime(ius); err == nil {
|
||||
// The Date-Modified header truncates sub-second precision, so
|
||||
// use mtime < t+1s instead of mtime <= t to check for unmodified.
|
||||
if modtime.Before(t.Add(1 * time.Second)) {
|
||||
return condTrue
|
||||
}
|
||||
return condFalse
|
||||
}
|
||||
return condNone
|
||||
}
|
||||
|
||||
func checkIfNoneMatch(ctx context.Context) condResult {
|
||||
inm := ctx.GetHeader("If-None-Match")
|
||||
if inm == "" {
|
||||
return condNone
|
||||
}
|
||||
buf := inm
|
||||
for {
|
||||
buf = textproto.TrimString(buf)
|
||||
if len(buf) == 0 {
|
||||
break
|
||||
}
|
||||
if buf[0] == ',' {
|
||||
buf = buf[1:]
|
||||
}
|
||||
if buf[0] == '*' {
|
||||
return condFalse
|
||||
}
|
||||
etag, remain := scanETag(buf)
|
||||
if etag == "" {
|
||||
break
|
||||
}
|
||||
if etagWeakMatch(etag, ctx.ResponseWriter().Header().Get("Etag")) {
|
||||
return condFalse
|
||||
}
|
||||
buf = remain
|
||||
}
|
||||
return condTrue
|
||||
}
|
||||
|
||||
func checkIfModifiedSince(ctx context.Context, modtime time.Time) condResult {
|
||||
if ctx.Method() != http.MethodGet && ctx.Method() != http.MethodHead {
|
||||
return condNone
|
||||
}
|
||||
ims := ctx.GetHeader("If-Modified-Since")
|
||||
if ims == "" || isZeroTime(modtime) {
|
||||
return condNone
|
||||
}
|
||||
t, err := http.ParseTime(ims)
|
||||
if err != nil {
|
||||
return condNone
|
||||
}
|
||||
// The Date-Modified header truncates sub-second precision, so
|
||||
// use mtime < t+1s instead of mtime <= t to check for unmodified.
|
||||
if modtime.Before(t.Add(1 * time.Second)) {
|
||||
return condFalse
|
||||
}
|
||||
return condTrue
|
||||
}
|
||||
|
||||
func checkIfRange(ctx context.Context, modtime time.Time) condResult {
|
||||
if ctx.Method() != http.MethodGet {
|
||||
return condNone
|
||||
}
|
||||
ir := ctx.GetHeader("If-Range")
|
||||
if ir == "" {
|
||||
return condNone
|
||||
}
|
||||
etag, _ := scanETag(ir)
|
||||
if etag != "" {
|
||||
if etagStrongMatch(etag, ctx.ResponseWriter().Header().Get("Etag")) {
|
||||
return condTrue
|
||||
}
|
||||
return condFalse
|
||||
|
||||
}
|
||||
// The If-Range value is typically the ETag value, but it may also be
|
||||
// the modtime date. See golang.org/issue/8367.
|
||||
if modtime.IsZero() {
|
||||
return condFalse
|
||||
}
|
||||
t, err := http.ParseTime(ir)
|
||||
if err != nil {
|
||||
return condFalse
|
||||
}
|
||||
if t.Unix() == modtime.Unix() {
|
||||
return condTrue
|
||||
}
|
||||
return condFalse
|
||||
}
|
||||
|
||||
var unixEpochTime = time.Unix(0, 0)
|
||||
|
||||
// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
|
||||
func isZeroTime(t time.Time) bool {
|
||||
return t.IsZero() || t.Equal(unixEpochTime)
|
||||
}
|
||||
|
||||
func setLastModified(ctx context.Context, modtime time.Time) {
|
||||
if !isZeroTime(modtime) {
|
||||
ctx.Header(lastModifiedHeaderKey, modtime.UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()))
|
||||
}
|
||||
}
|
||||
|
||||
func writeNotModified(ctx context.Context) {
|
||||
// RFC 7232 section 4.1:
|
||||
// a sender SHOULD NOT generate representation metadata other than the
|
||||
// above listed fields unless said metadata exists for the purpose of
|
||||
// guiding cache updates (e.g., Last-Modified might be useful if the
|
||||
// response does not have an ETag field).
|
||||
h := ctx.ResponseWriter().Header()
|
||||
delete(h, contentTypeHeaderKey)
|
||||
|
||||
delete(h, contentLengthHeaderKey)
|
||||
if h.Get("Etag") != "" {
|
||||
delete(h, "Last-Modified")
|
||||
}
|
||||
ctx.StatusCode(http.StatusNotModified)
|
||||
}
|
||||
|
||||
// checkPreconditions evaluates request preconditions and reports whether a precondition
|
||||
// resulted in sending StatusNotModified or StatusPreconditionFailed.
|
||||
func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rangeHeader string) {
|
||||
// This function carefully follows RFC 7232 section 6.
|
||||
ch := checkIfMatch(ctx)
|
||||
if ch == condNone {
|
||||
ch = checkIfUnmodifiedSince(ctx, modtime)
|
||||
}
|
||||
if ch == condFalse {
|
||||
|
||||
ctx.StatusCode(http.StatusPreconditionFailed)
|
||||
return true, ""
|
||||
}
|
||||
switch checkIfNoneMatch(ctx) {
|
||||
case condFalse:
|
||||
if ctx.Method() == http.MethodGet || ctx.Method() == http.MethodHead {
|
||||
writeNotModified(ctx)
|
||||
return true, ""
|
||||
}
|
||||
ctx.StatusCode(http.StatusPreconditionFailed)
|
||||
return true, ""
|
||||
|
||||
case condNone:
|
||||
if checkIfModifiedSince(ctx, modtime) == condFalse {
|
||||
writeNotModified(ctx)
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
|
||||
rangeHeader = ctx.GetHeader("Range")
|
||||
if rangeHeader != "" {
|
||||
if checkIfRange(ctx, modtime) == condFalse {
|
||||
rangeHeader = ""
|
||||
}
|
||||
}
|
||||
return false, rangeHeader
|
||||
}
|
||||
|
||||
// name is '/'-separated, not filepath.Separator.
|
||||
func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bool, showList bool, gzip bool) (string, int) {
|
||||
const indexPage = "/index.html"
|
||||
|
||||
// redirect .../index.html to .../
|
||||
// can't use Redirect() because that would make the path absolute,
|
||||
// which would be a problem running under StripPrefix
|
||||
if strings.HasSuffix(ctx.Request().URL.Path, indexPage) {
|
||||
localRedirect(ctx, "./")
|
||||
return "", http.StatusMovedPermanently
|
||||
}
|
||||
|
||||
f, err := fs.Open(name)
|
||||
if err != nil {
|
||||
msg, code := toHTTPError(err)
|
||||
return msg, code
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
d, err := f.Stat()
|
||||
if err != nil {
|
||||
msg, code := toHTTPError(err)
|
||||
return msg, code
|
||||
|
||||
}
|
||||
|
||||
if redirect {
|
||||
// redirect to canonical path: / at end of directory url
|
||||
// ctx.Request.URL.Path always begins with /
|
||||
url := ctx.Request().URL.Path
|
||||
if d.IsDir() {
|
||||
if url[len(url)-1] != '/' {
|
||||
localRedirect(ctx, path.Base(url)+"/")
|
||||
return "", http.StatusMovedPermanently
|
||||
}
|
||||
} else {
|
||||
if url[len(url)-1] == '/' {
|
||||
localRedirect(ctx, "../"+path.Base(url))
|
||||
return "", http.StatusMovedPermanently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// redirect if the directory name doesn't end in a slash
|
||||
if d.IsDir() {
|
||||
url := ctx.Request().URL.Path
|
||||
if url[len(url)-1] != '/' {
|
||||
localRedirect(ctx, path.Base(url)+"/")
|
||||
return "", http.StatusMovedPermanently
|
||||
}
|
||||
}
|
||||
|
||||
// use contents of index.html for directory, if present
|
||||
if d.IsDir() {
|
||||
index := strings.TrimSuffix(name, "/") + indexPage
|
||||
ff, err := fs.Open(index)
|
||||
if err == nil {
|
||||
defer ff.Close()
|
||||
dd, err := ff.Stat()
|
||||
if err == nil {
|
||||
name = index
|
||||
d = dd
|
||||
f = ff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Still a directory? (we didn't find an index.html file)
|
||||
if d.IsDir() {
|
||||
if !showList {
|
||||
return "", http.StatusForbidden
|
||||
}
|
||||
if checkIfModifiedSince(ctx, d.ModTime()) == condFalse {
|
||||
writeNotModified(ctx)
|
||||
return "", http.StatusNotModified
|
||||
}
|
||||
ctx.Header("Last-Modified", d.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()))
|
||||
return dirList(ctx, f)
|
||||
|
||||
}
|
||||
|
||||
// serveContent will check modification time
|
||||
sizeFunc := func() (int64, error) { return d.Size(), nil }
|
||||
return serveContent(ctx, d.Name(), d.ModTime(), sizeFunc, f, gzip)
|
||||
}
|
||||
|
||||
// toHTTPError returns a non-specific HTTP error message and status code
|
||||
// for a given non-nil error value. It's important that toHTTPError does not
|
||||
// actually return err.Error(), since msg and httpStatus are returned to users,
|
||||
// and historically Go's ServeContent always returned just "404 Not Found" for
|
||||
// all errors. We don't want to start leaking information in error messages.
|
||||
func toHTTPError(err error) (msg string, httpStatus int) {
|
||||
if os.IsNotExist(err) {
|
||||
return "404 page not found", http.StatusNotFound
|
||||
}
|
||||
if os.IsPermission(err) {
|
||||
return "403 Forbidden", http.StatusForbidden
|
||||
}
|
||||
// Default:
|
||||
return "500 Internal Server Error", http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// localRedirect gives a Moved Permanently response.
|
||||
// It does not convert relative paths to absolute paths like Redirect does.
|
||||
func localRedirect(ctx context.Context, newPath string) {
|
||||
if q := ctx.Request().URL.RawQuery; q != "" {
|
||||
newPath += "?" + q
|
||||
}
|
||||
ctx.Header("Location", newPath)
|
||||
ctx.StatusCode(http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func containsDotDot(v string) bool {
|
||||
if !strings.Contains(v, "..") {
|
||||
return false
|
||||
}
|
||||
for _, ent := range strings.FieldsFunc(v, isSlashRune) {
|
||||
if ent == ".." {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
|
||||
|
||||
// httpRange specifies the byte range to be sent to the client.
|
||||
type httpRange struct {
|
||||
start, length int64
|
||||
}
|
||||
|
||||
func (r httpRange) contentRange(size int64) string {
|
||||
return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size)
|
||||
}
|
||||
|
||||
func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader {
|
||||
return textproto.MIMEHeader{
|
||||
"Content-Range": {r.contentRange(size)},
|
||||
contentType: {contentType},
|
||||
}
|
||||
}
|
||||
|
||||
// parseRange parses a Range header string as per RFC 2616.
|
||||
// errNoOverlap is returned if none of the ranges overlap.
|
||||
func parseRange(s string, size int64) ([]httpRange, error) {
|
||||
if s == "" {
|
||||
return nil, nil // header not present
|
||||
}
|
||||
const b = "bytes="
|
||||
if !strings.HasPrefix(s, b) {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
var ranges []httpRange
|
||||
noOverlap := false
|
||||
for _, ra := range strings.Split(s[len(b):], ",") {
|
||||
ra = strings.TrimSpace(ra)
|
||||
if ra == "" {
|
||||
continue
|
||||
}
|
||||
i := strings.Index(ra, "-")
|
||||
if i < 0 {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:])
|
||||
var r httpRange
|
||||
if start == "" {
|
||||
// If no start is specified, end specifies the
|
||||
// range start relative to the end of the file.
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
if i > size {
|
||||
i = size
|
||||
}
|
||||
r.start = size - i
|
||||
r.length = size - r.start
|
||||
} else {
|
||||
i, err := strconv.ParseInt(start, 10, 64)
|
||||
if err != nil || i < 0 {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
if i >= size {
|
||||
// If the range begins after the size of the content,
|
||||
// then it does not overlap.
|
||||
noOverlap = true
|
||||
continue
|
||||
}
|
||||
r.start = i
|
||||
if end == "" {
|
||||
// If no end is specified, range extends to end of the file.
|
||||
r.length = size - r.start
|
||||
} else {
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if err != nil || r.start > i {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
if i >= size {
|
||||
i = size - 1
|
||||
}
|
||||
r.length = i - r.start + 1
|
||||
}
|
||||
}
|
||||
ranges = append(ranges, r)
|
||||
}
|
||||
if noOverlap && len(ranges) == 0 {
|
||||
// The specified ranges did not overlap with the content.
|
||||
return nil, errNoOverlap
|
||||
}
|
||||
return ranges, nil
|
||||
}
|
||||
|
||||
// countingWriter counts how many bytes have been written to it.
|
||||
type countingWriter int64
|
||||
|
||||
func (w *countingWriter) Write(p []byte) (n int, err error) {
|
||||
*w += countingWriter(len(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// rangesMIMESize returns the number of bytes it takes to encode the
|
||||
// provided ranges as a multipart response.
|
||||
func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) {
|
||||
var w countingWriter
|
||||
mw := multipart.NewWriter(&w)
|
||||
for _, ra := range ranges {
|
||||
mw.CreatePart(ra.mimeHeader(contentType, contentSize))
|
||||
encSize += ra.length
|
||||
}
|
||||
mw.Close()
|
||||
encSize += int64(w)
|
||||
return
|
||||
}
|
||||
|
||||
func sumRangesSize(ranges []httpRange) (size int64) {
|
||||
for _, ra := range ranges {
|
||||
size += ra.length
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DirectoryExists returns true if a directory(or file) exists, otherwise false
|
||||
func DirectoryExists(dir string) bool {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
252
core/router/handler.go
Normal file
252
core/router/handler.go
Normal file
@@ -0,0 +1,252 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"html"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/nettools"
|
||||
"github.com/kataras/iris/core/router/httprouter"
|
||||
)
|
||||
|
||||
// RequestHandler the middle man between acquiring a context and releasing it.
|
||||
// By-default is the router algorithm.
|
||||
type RequestHandler interface {
|
||||
// HandleRequest is same as context.Handler but its usage is only about routing,
|
||||
// separate the concept here.
|
||||
HandleRequest(context.Context)
|
||||
// Build should builds the handler, it's being called on router's BuildRouter.
|
||||
Build(provider RoutesProvider) error
|
||||
}
|
||||
|
||||
type tree struct {
|
||||
Method string
|
||||
// subdomain is empty for default-hostname routes,
|
||||
// ex: mysubdomain.
|
||||
Subdomain string
|
||||
Entry *httprouter.Node
|
||||
}
|
||||
|
||||
type routerHandler struct {
|
||||
trees []*tree
|
||||
vhost atomic.Value // is a string setted at the first it founds a subdomain, we need that here in order to reduce the resolveVHost calls
|
||||
hosts bool // true if at least one route contains a Subdomain.
|
||||
}
|
||||
|
||||
var _ RequestHandler = &routerHandler{}
|
||||
|
||||
func (h *routerHandler) getTree(method, subdomain string) *tree {
|
||||
for i := range h.trees {
|
||||
t := h.trees[i]
|
||||
if t.Method == method && t.Subdomain == subdomain {
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *routerHandler) addRoute(method, subdomain, path string, handlers context.Handlers) error {
|
||||
// get or create a tree and add the route
|
||||
t := h.getTree(method, subdomain)
|
||||
|
||||
if t == nil {
|
||||
//first time we register a route to this method with this domain
|
||||
t = &tree{Method: method, Subdomain: subdomain, Entry: new(httprouter.Node)}
|
||||
h.trees = append(h.trees, t)
|
||||
}
|
||||
|
||||
if err := t.Entry.AddRoute(path, handlers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDefaultHandler() RequestHandler {
|
||||
h := &routerHandler{}
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *routerHandler) Build(provider RoutesProvider) error {
|
||||
registeredRoutes := provider.GetRoutes()
|
||||
h.trees = h.trees[0:0] // reset, inneed when rebuilding.
|
||||
|
||||
// sort, subdomains goes first.
|
||||
sort.Slice(registeredRoutes, func(i, j int) bool {
|
||||
return len(registeredRoutes[i].Subdomain) >= len(registeredRoutes[j].Subdomain)
|
||||
})
|
||||
|
||||
for _, r := range registeredRoutes {
|
||||
if r.Subdomain != "" {
|
||||
h.hosts = true
|
||||
}
|
||||
// the only "bad" with this is if the user made an error
|
||||
// on route, it will be stacked shown in this build state
|
||||
// and no in the lines of the user's action, they should read
|
||||
// the docs better. Or TODO: add a link here in order to help new users.
|
||||
if err := h.addRoute(r.Method, r.Subdomain, r.Path, r.Handlers); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *routerHandler) HandleRequest(ctx context.Context) {
|
||||
method := ctx.Method()
|
||||
|
||||
for i := range h.trees {
|
||||
t := h.trees[i]
|
||||
if method != t.Method {
|
||||
continue
|
||||
}
|
||||
|
||||
// Changed my mind for subdomains, there are unnecessary steps here
|
||||
// most servers don't need these and on other servers may force the server to send a 404 not found
|
||||
// on a valid subdomain, by commenting my previous implementation we allow any request host to be discovarable for subdomains.
|
||||
// if h.hosts && t.Subdomain != "" {
|
||||
|
||||
// if h.vhost.Load() == nil {
|
||||
// h.vhost.Store(nettools.ResolveVHost(ctx.Application().ConfigurationReadOnly().GetAddr()))
|
||||
// }
|
||||
|
||||
// host := h.vhost.Load().(string)
|
||||
// requestHost := ctx.Host()
|
||||
|
||||
// if requestHost != host {
|
||||
// // we have a subdomain
|
||||
// if strings.Contains(t.Subdomain, DynamicSubdomainIndicator) {
|
||||
// } else {
|
||||
// // if subdomain+host is not the request host
|
||||
// // and
|
||||
// // if request host didn't matched the server's host
|
||||
// // check if reached the server
|
||||
// // with a local address, this case is the developer him/herself,
|
||||
// // if both of them failed then continue and ignore this tree.
|
||||
// if t.Subdomain+host != requestHost && !nettools.IsLoopbackHost(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 not the same as the vhost)
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
// new, simpler and without the need of known the real host:
|
||||
if h.hosts && t.Subdomain != "" {
|
||||
requestHost := ctx.Host()
|
||||
if nettools.IsLoopbackSubdomain(requestHost) {
|
||||
// this fixes a bug when listening on
|
||||
// 127.0.0.1:8080 for example
|
||||
// and have a wildcard subdomain and a route registered to root domain.
|
||||
continue // it's not a subdomain, it's something like 127.0.0.1 probably
|
||||
}
|
||||
// it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty
|
||||
if t.Subdomain == DynamicSubdomainIndicator {
|
||||
// mydomain.com -> invalid
|
||||
// localhost -> invalid
|
||||
// sub.mydomain.com -> valid
|
||||
// sub.localhost -> valid
|
||||
serverHost := ctx.Application().ConfigurationReadOnly().GetVHost()
|
||||
if serverHost == requestHost {
|
||||
continue // it's not a subdomain, it's a full domain (with .com...)
|
||||
}
|
||||
|
||||
dotIdx := strings.IndexByte(requestHost, '.')
|
||||
slashIdx := strings.IndexByte(requestHost, '/')
|
||||
if dotIdx > 0 && (slashIdx == -1 || slashIdx > dotIdx) {
|
||||
// if "." was found anywhere but not at the first path segment (host).
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
// continue to that, any subdomain is valid.
|
||||
} else if !strings.HasPrefix(requestHost, t.Subdomain) { // t.Subdomain contains the dot.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
handlers, params, mustRedirect := t.Entry.ResolveRoute(ctx)
|
||||
if len(handlers) > 0 {
|
||||
ctx.SetParams(params)
|
||||
ctx.SetHandlers(handlers)
|
||||
ctx.Handlers()[0](ctx)
|
||||
// to remove the .Next(maybe not a good idea), reduces the performance a bit:
|
||||
// ctx.Handlers()[0](ctx) // execute the first, as soon as possible
|
||||
// // execute the chain of handlers, carefully
|
||||
// current := ctx.HandlerIndex(-1)
|
||||
// for {
|
||||
// if ctx.IsStopped() || current >= n {
|
||||
// break
|
||||
// }
|
||||
|
||||
// ctx.HandlerIndex(current)
|
||||
// ctx.Handlers()[current](ctx)
|
||||
// current++
|
||||
// if i := ctx.HandlerIndex(-1); i > current { // navigate to previous handler is not allowed
|
||||
// current = i
|
||||
// }
|
||||
// }
|
||||
|
||||
return
|
||||
} else if mustRedirect && !ctx.Application().ConfigurationReadOnly().GetDisablePathCorrection() { // && ctx.Method() == MethodConnect {
|
||||
urlToRedirect := ctx.Path()
|
||||
pathLen := len(urlToRedirect)
|
||||
|
||||
if pathLen > 1 {
|
||||
if urlToRedirect[pathLen-1] == '/' {
|
||||
urlToRedirect = urlToRedirect[:pathLen-1] // remove the last /
|
||||
} else {
|
||||
// it has path prefix, it doesn't ends with / and it hasn't be found, then just append the slash
|
||||
urlToRedirect = urlToRedirect + "/"
|
||||
}
|
||||
|
||||
statusForRedirect := http.StatusMovedPermanently // StatusMovedPermanently, this document is obselte, clients caches this.
|
||||
if t.Method == http.MethodPost ||
|
||||
t.Method == http.MethodPut ||
|
||||
t.Method == http.MethodDelete {
|
||||
statusForRedirect = http.StatusTemporaryRedirect // To maintain POST data
|
||||
}
|
||||
|
||||
ctx.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 t.Method == http.MethodGet {
|
||||
note := "<a href=\"" +
|
||||
html.EscapeString(urlToRedirect) +
|
||||
"\">Moved Permanently</a>.\n"
|
||||
|
||||
ctx.ResponseWriter().WriteString(note)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
// not found
|
||||
break
|
||||
}
|
||||
|
||||
if ctx.Application().ConfigurationReadOnly().GetFireMethodNotAllowed() {
|
||||
var methodAllowed string
|
||||
for i := range h.trees {
|
||||
t := h.trees[i]
|
||||
methodAllowed = t.Method // keep track of the allowed method of the last checked tree
|
||||
if ctx.Method() != methodAllowed {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
ctx.Header("Allow", methodAllowed)
|
||||
ctx.StatusCode(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
ctx.StatusCode(http.StatusNotFound)
|
||||
}
|
||||
56
core/router/httprouter/LICENSE
Normal file
56
core/router/httprouter/LICENSE
Normal file
@@ -0,0 +1,56 @@
|
||||
Copyright (c) 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Gerasimos Maropoulos nor the name of his
|
||||
username, kataras, may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
Third-Parties:
|
||||
|
||||
Copyright (c) 2013 Julien Schmidt. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* The names of the contributors may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL JULIEN SCHMIDT BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
456
core/router/httprouter/node.go
Normal file
456
core/router/httprouter/node.go
Normal file
@@ -0,0 +1,456 @@
|
||||
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of the below source code is governed by the BSD 3-Clause license.
|
||||
|
||||
package httprouter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/errors"
|
||||
)
|
||||
|
||||
func min(a, b int) int {
|
||||
if a <= b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func countParams(path string) uint8 {
|
||||
var n uint
|
||||
for i := 0; i < len(path); i++ {
|
||||
if path[i] != ':' && path[i] != '*' {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
}
|
||||
if n >= 255 {
|
||||
return 255
|
||||
}
|
||||
return uint8(n)
|
||||
}
|
||||
|
||||
type nodeType uint8
|
||||
|
||||
const (
|
||||
static nodeType = iota // default
|
||||
root
|
||||
param
|
||||
catchAll
|
||||
)
|
||||
|
||||
// Node is the default request handler's tree's entry type.
|
||||
// Examples of its algorithm can be found via googling or via youtube
|
||||
// search term: trie, tree sort, data structures: tree, reversed tree, sort tree etc...
|
||||
type Node struct {
|
||||
path string
|
||||
wildChild bool
|
||||
nType nodeType
|
||||
maxParams uint8
|
||||
indices string
|
||||
children []*Node
|
||||
handle context.Handlers
|
||||
priority uint32
|
||||
}
|
||||
|
||||
// increments priority of the given child and reorders if necessary
|
||||
func (n *Node) incrementChildPrio(pos int) int {
|
||||
n.children[pos].priority++
|
||||
prio := n.children[pos].priority
|
||||
|
||||
// adjust position (move to front)
|
||||
newPos := pos
|
||||
for newPos > 0 && n.children[newPos-1].priority < prio {
|
||||
// swap Node positions
|
||||
n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1]
|
||||
|
||||
newPos--
|
||||
}
|
||||
|
||||
// build new index char string
|
||||
if newPos != pos {
|
||||
n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
|
||||
n.indices[pos:pos+1] + // the index char we move
|
||||
n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
|
||||
}
|
||||
|
||||
return newPos
|
||||
}
|
||||
|
||||
// AddRoute adds a route with the given handler to the path.
|
||||
func (n *Node) AddRoute(path string, handle context.Handlers) error {
|
||||
fullPath := path
|
||||
n.priority++
|
||||
numParams := countParams(path)
|
||||
|
||||
// non-empty tree
|
||||
if len(n.path) > 0 || len(n.children) > 0 {
|
||||
walk:
|
||||
for {
|
||||
// Update maxParams of the current Node
|
||||
if numParams > n.maxParams {
|
||||
n.maxParams = numParams
|
||||
}
|
||||
|
||||
// Find the longest common prefix.
|
||||
// This also implies that the common prefix contains no ':' or '*'
|
||||
// since the existing key can't contain those chars.
|
||||
i := 0
|
||||
max := min(len(path), len(n.path))
|
||||
for i < max && path[i] == n.path[i] {
|
||||
i++
|
||||
}
|
||||
|
||||
// Split edge
|
||||
if i < len(n.path) {
|
||||
child := Node{
|
||||
path: n.path[i:],
|
||||
wildChild: n.wildChild,
|
||||
nType: static,
|
||||
indices: n.indices,
|
||||
children: n.children,
|
||||
handle: n.handle,
|
||||
priority: n.priority - 1,
|
||||
}
|
||||
|
||||
// Update maxParams (max of all children)
|
||||
for i := range child.children {
|
||||
if child.children[i].maxParams > child.maxParams {
|
||||
child.maxParams = child.children[i].maxParams
|
||||
}
|
||||
}
|
||||
|
||||
n.children = []*Node{&child}
|
||||
// []byte for proper unicode char conversion, see #65
|
||||
n.indices = string([]byte{n.path[i]})
|
||||
n.path = path[:i]
|
||||
n.handle = nil
|
||||
n.wildChild = false
|
||||
}
|
||||
|
||||
// Make new Node a child of this Node
|
||||
if i < len(path) {
|
||||
path = path[i:]
|
||||
|
||||
if n.wildChild {
|
||||
n = n.children[0]
|
||||
n.priority++
|
||||
|
||||
// Update maxParams of the child Node
|
||||
if numParams > n.maxParams {
|
||||
n.maxParams = numParams
|
||||
}
|
||||
numParams--
|
||||
|
||||
// Check if the wildcard matches
|
||||
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
|
||||
// Check for longer wildcard, e.g. :name and :names
|
||||
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
|
||||
continue walk
|
||||
} else {
|
||||
// Wildcard conflict
|
||||
pathSeg := strings.SplitN(path, "/", 2)[0]
|
||||
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
||||
return errors.New("'" + pathSeg +
|
||||
"' in new path '" + fullPath +
|
||||
"' conflicts with existing wildcard '" + n.path +
|
||||
"' in existing prefix '" + prefix +
|
||||
"'")
|
||||
}
|
||||
}
|
||||
|
||||
c := path[0]
|
||||
|
||||
// slash after param
|
||||
if n.nType == param && c == '/' && len(n.children) == 1 {
|
||||
n = n.children[0]
|
||||
n.priority++
|
||||
continue walk
|
||||
}
|
||||
|
||||
// Check if a child with the next path byte exists
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
if c == n.indices[i] {
|
||||
i = n.incrementChildPrio(i)
|
||||
n = n.children[i]
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise insert it
|
||||
if c != ':' && c != '*' {
|
||||
// []byte for proper unicode char conversion, see #65
|
||||
n.indices += string([]byte{c})
|
||||
child := &Node{
|
||||
maxParams: numParams,
|
||||
}
|
||||
n.children = append(n.children, child)
|
||||
n.incrementChildPrio(len(n.indices) - 1)
|
||||
n = child
|
||||
}
|
||||
return n.insertChild(numParams, path, fullPath, handle)
|
||||
|
||||
} else if i == len(path) { // Make Node a (in-path) leaf
|
||||
if n.handle != nil {
|
||||
return errors.New("a handle is already registered for path '" + fullPath + "'")
|
||||
}
|
||||
n.handle = handle
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} else { // Empty tree
|
||||
n.insertChild(numParams, path, fullPath, handle)
|
||||
n.nType = root
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Node) insertChild(numParams uint8, path, fullPath string, handle context.Handlers) error {
|
||||
var offset int // already handled bytes of the path
|
||||
|
||||
// find prefix until first wildcard (beginning with ':'' or '*'')
|
||||
for i, max := 0, len(path); numParams > 0; i++ {
|
||||
c := path[i]
|
||||
if c != ':' && c != '*' {
|
||||
continue
|
||||
}
|
||||
|
||||
// find wildcard end (either '/' or path end)
|
||||
end := i + 1
|
||||
for end < max && path[end] != '/' {
|
||||
switch path[end] {
|
||||
// the wildcard name must not contain ':' and '*'
|
||||
case ':', '*':
|
||||
return errors.New("only one wildcard per path segment is allowed, has: '" +
|
||||
path[i:] + "' in path '" + fullPath + "'")
|
||||
default:
|
||||
end++
|
||||
}
|
||||
}
|
||||
|
||||
// check if this Node existing children which would be
|
||||
// unreachable if we insert the wildcard here
|
||||
if len(n.children) > 0 {
|
||||
return errors.New("wildcard route '" + path[i:end] +
|
||||
"' conflicts with existing children in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
// check if the wildcard has a name
|
||||
if end-i < 2 {
|
||||
return errors.New("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
if c == ':' { // param
|
||||
// split path at the beginning of the wildcard
|
||||
if i > 0 {
|
||||
n.path = path[offset:i]
|
||||
offset = i
|
||||
}
|
||||
|
||||
child := &Node{
|
||||
nType: param,
|
||||
maxParams: numParams,
|
||||
}
|
||||
n.children = []*Node{child}
|
||||
n.wildChild = true
|
||||
n = child
|
||||
n.priority++
|
||||
numParams--
|
||||
|
||||
// if the path doesn't end with the wildcard, then there
|
||||
// will be another non-wildcard subpath starting with '/'
|
||||
if end < max {
|
||||
n.path = path[offset:end]
|
||||
offset = end
|
||||
|
||||
child := &Node{
|
||||
maxParams: numParams,
|
||||
priority: 1,
|
||||
}
|
||||
n.children = []*Node{child}
|
||||
n = child
|
||||
}
|
||||
|
||||
} else { // catchAll
|
||||
if end != max || numParams > 1 {
|
||||
return errors.New("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||||
return errors.New("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
// currently fixed width 1 for '/'
|
||||
i--
|
||||
if path[i] != '/' {
|
||||
return errors.New("no / before catch-all in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
n.path = path[offset:i]
|
||||
|
||||
// first Node: catchAll Node with empty path
|
||||
child := &Node{
|
||||
wildChild: true,
|
||||
nType: catchAll,
|
||||
maxParams: 1,
|
||||
}
|
||||
n.children = []*Node{child}
|
||||
n.indices = string(path[i])
|
||||
n = child
|
||||
n.priority++
|
||||
|
||||
// second Node: Node holding the variable
|
||||
child = &Node{
|
||||
path: path[i:],
|
||||
nType: catchAll,
|
||||
maxParams: 1,
|
||||
handle: handle,
|
||||
priority: 1,
|
||||
}
|
||||
n.children = []*Node{child}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// insert remaining path part and handle to the leaf
|
||||
n.path = path[offset:]
|
||||
n.handle = handle
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveRoute sets the handlers registered to a given path which is acquiring by ctx.Path().
|
||||
// The values of
|
||||
// wildcards are saved to the context's Values.
|
||||
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
|
||||
// made if a handle exists with an extra (without the) trailing slash for the
|
||||
// given context.
|
||||
//
|
||||
// ResolveRoute finds the correct registered route from the Node when the ctx.Handlers() > 0.
|
||||
func (n *Node) ResolveRoute(ctx context.Context) (handlers context.Handlers, p context.RequestParams, tsr bool) { //(p context.RequestParams, tsr bool) {
|
||||
path := ctx.Request().URL.Path
|
||||
handlers = ctx.Handlers()
|
||||
// values := ctx.Values()
|
||||
p = ctx.Params()
|
||||
walk: // outer loop for walking the tree
|
||||
for {
|
||||
if len(path) > len(n.path) {
|
||||
if path[:len(n.path)] == n.path {
|
||||
path = path[len(n.path):]
|
||||
// If this Node does not have a wildcard (param or catchAll)
|
||||
// child, we can just look up the next child Node and continue
|
||||
// to walk down the tree
|
||||
if !n.wildChild {
|
||||
c := path[0]
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
if c == n.indices[i] {
|
||||
n = n.children[i]
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found.
|
||||
// We can recommend to redirect to the same URL without a
|
||||
// trailing slash if a leaf exists for that path.
|
||||
tsr = (path == "/" && n.handle != nil)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// handle wildcard child
|
||||
n = n.children[0]
|
||||
switch n.nType {
|
||||
case param:
|
||||
// find param end (either '/' or path end)
|
||||
end := 0
|
||||
for end < len(path) && path[end] != '/' {
|
||||
end++
|
||||
}
|
||||
|
||||
// save param value
|
||||
if cap(p) < int(n.maxParams) {
|
||||
p = make(context.RequestParams, 0, n.maxParams)
|
||||
}
|
||||
i := len(p)
|
||||
p = p[:i+1] // expand
|
||||
p[i].Key = n.path[1:]
|
||||
p[i].Value = path[:end]
|
||||
|
||||
// we need to go deeper!
|
||||
if end < len(path) {
|
||||
if len(n.children) > 0 {
|
||||
path = path[end:]
|
||||
n = n.children[0]
|
||||
continue walk
|
||||
}
|
||||
|
||||
// ... but we can't
|
||||
tsr = (len(path) == end+1)
|
||||
return
|
||||
}
|
||||
|
||||
if handlers = n.handle; handlers != nil {
|
||||
return
|
||||
} else if len(n.children) == 1 {
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists for TSR recommendation
|
||||
n = n.children[0]
|
||||
tsr = (n.path == "/" && n.handle != nil)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
case catchAll:
|
||||
// save param value
|
||||
if cap(p) < int(n.maxParams) {
|
||||
p = make(context.RequestParams, 0, n.maxParams)
|
||||
}
|
||||
i := len(p)
|
||||
p = p[:i+1] // expand
|
||||
|
||||
p[i].Key = n.path[2:]
|
||||
p[i].Value = path
|
||||
handlers = n.handle
|
||||
return
|
||||
|
||||
default:
|
||||
// invalid Node type here
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if path == n.path {
|
||||
// We should have reached the Node containing the handle.
|
||||
// Check if this Node has a handle registered.
|
||||
if handlers = n.handle; handlers != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if path == "/" && n.wildChild && n.nType != root {
|
||||
tsr = true
|
||||
return
|
||||
}
|
||||
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists for trailing slash recommendation
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
if n.indices[i] == '/' {
|
||||
n = n.children[i]
|
||||
tsr = (len(n.path) == 1 && n.handle != nil) ||
|
||||
(n.nType == catchAll && n.children[0].handle != nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Nothing found. We can recommend to redirect to the same URL with an
|
||||
// extra trailing slash if a leaf exists for that path
|
||||
tsr = (path == "/") ||
|
||||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
|
||||
path == n.path[:len(n.path)-1] && n.handle != nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
247
core/router/macro.go
Normal file
247
core/router/macro.go
Normal file
@@ -0,0 +1,247 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/errors"
|
||||
"github.com/kataras/iris/core/router/macro"
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
)
|
||||
|
||||
// defaultMacros returns a new macro map which
|
||||
// contains the default router's named param types functions.
|
||||
func defaultMacros() *macro.MacroMap {
|
||||
macros := macro.NewMacroMap()
|
||||
// registers the String and Int default macro funcs
|
||||
// user can add or override of his own funcs later on
|
||||
// i.e:
|
||||
// app.Macro.String.RegisterFunc("equal", func(eqWith string) func(string) bool {
|
||||
// return func(paramValue string) bool {
|
||||
// return eqWith == paramValue
|
||||
// }})
|
||||
registerBuiltinsMacroFuncs(macros)
|
||||
|
||||
return macros
|
||||
}
|
||||
|
||||
func registerBuiltinsMacroFuncs(out *macro.MacroMap) {
|
||||
// register the String which is the default type if not
|
||||
// parameter type is specified or
|
||||
// if a given parameter into path given but the func doesn't exist on the
|
||||
// parameter type's function list.
|
||||
//
|
||||
// these can be overridden by the user, later on.
|
||||
registerStringMacroFuncs(out.String)
|
||||
registerIntMacroFuncs(out.Int)
|
||||
registerAlphabeticalMacroFuncs(out.Alphabetical)
|
||||
registerFileMacroFuncs(out.File)
|
||||
registerPathMacroFuncs(out.Path)
|
||||
}
|
||||
|
||||
// String
|
||||
// anything one part
|
||||
func registerStringMacroFuncs(out *macro.Macro) {
|
||||
// this can be used everywhere, it's to help users to define custom regexp expressions
|
||||
// on all macros
|
||||
out.RegisterFunc("regexp", func(expr string) macro.EvaluatorFunc {
|
||||
regexpEvaluator := macro.MustNewEvaluatorFromRegexp(expr)
|
||||
return regexpEvaluator
|
||||
})
|
||||
|
||||
// checks if param value starts with the 'prefix' arg
|
||||
out.RegisterFunc("prefix", func(prefix string) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
return strings.HasPrefix(paramValue, prefix)
|
||||
}
|
||||
})
|
||||
|
||||
// checks if param value ends with the 'suffix' arg
|
||||
out.RegisterFunc("suffix", func(suffix string) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
return strings.HasSuffix(paramValue, suffix)
|
||||
}
|
||||
})
|
||||
|
||||
// checks if param value contains the 's' arg
|
||||
out.RegisterFunc("contains", func(s string) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
return strings.Contains(paramValue, s)
|
||||
}
|
||||
})
|
||||
|
||||
// checks if param value's length is at least 'min'
|
||||
out.RegisterFunc("min", func(min int) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
return len(paramValue) >= min
|
||||
}
|
||||
})
|
||||
// checks if param value's length is not bigger than 'max'
|
||||
out.RegisterFunc("max", func(max int) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
return max >= len(paramValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Int
|
||||
// only numbers (0-9)
|
||||
func registerIntMacroFuncs(out *macro.Macro) {
|
||||
// checks if the param value's int representation is
|
||||
// bigger or equal than 'min'
|
||||
out.RegisterFunc("min", func(min int) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
n, err := strconv.Atoi(paramValue)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return n >= min
|
||||
}
|
||||
})
|
||||
|
||||
// checks if the param value's int representation is
|
||||
// smaller or equal than 'max'
|
||||
out.RegisterFunc("max", func(max int) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
n, err := strconv.Atoi(paramValue)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return n <= max
|
||||
}
|
||||
})
|
||||
|
||||
// checks if the param value's int representation is
|
||||
// between min and max, including 'min' and 'max'
|
||||
out.RegisterFunc("range", func(min, max int) macro.EvaluatorFunc {
|
||||
return func(paramValue string) bool {
|
||||
n, err := strconv.Atoi(paramValue)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if n < min || n > max {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Alphabetical
|
||||
// letters only (upper or lowercase)
|
||||
func registerAlphabeticalMacroFuncs(out *macro.Macro) {
|
||||
|
||||
}
|
||||
|
||||
// File
|
||||
// letters (upper or lowercase)
|
||||
// numbers (0-9)
|
||||
// underscore (_)
|
||||
// dash (-)
|
||||
// point (.)
|
||||
// no spaces! or other character
|
||||
func registerFileMacroFuncs(out *macro.Macro) {
|
||||
|
||||
}
|
||||
|
||||
// Path
|
||||
// File+slashes(anywhere)
|
||||
// should be the latest param, it's the wildcard
|
||||
func registerPathMacroFuncs(out *macro.Macro) {
|
||||
|
||||
}
|
||||
|
||||
// compileRoutePathAndHandlers receives a route info and returns its parsed/"compiled" path
|
||||
// and the new handlers (prepend all the macro's handler, if any).
|
||||
//
|
||||
// It's not exported for direct use.
|
||||
func compileRoutePathAndHandlers(handlers context.Handlers, tmpl *macro.Template) (string, context.Handlers, error) {
|
||||
// parse the path to node's path, now.
|
||||
path, err := convertTmplToNodePath(tmpl)
|
||||
if err != nil {
|
||||
return tmpl.Src, handlers, err
|
||||
}
|
||||
// prepend the macro handler to the route, now,
|
||||
// right before the register to the tree, so routerbuilder.UseGlobal will work as expected.
|
||||
if len(tmpl.Params) > 0 {
|
||||
macroEvaluatorHandler := convertTmplToHandler(tmpl)
|
||||
// may return nil if no really need a macro handler evaluator
|
||||
if macroEvaluatorHandler != nil {
|
||||
handlers = append(context.Handlers{macroEvaluatorHandler}, handlers...)
|
||||
}
|
||||
}
|
||||
|
||||
return path, handlers, nil
|
||||
}
|
||||
|
||||
func convertTmplToNodePath(tmpl *macro.Template) (string, error) {
|
||||
routePath := tmpl.Src
|
||||
// if it has started with {} and it's valid
|
||||
// then the tmpl.Params will be filled,
|
||||
// so no any further check needed
|
||||
for i, p := range tmpl.Params {
|
||||
if p.Type == ast.ParamTypePath {
|
||||
if i != len(tmpl.Params)-1 {
|
||||
return "", errors.New("parameter type \"ParamTypePath\" is allowed to exists to the very last of a path")
|
||||
}
|
||||
routePath = strings.Replace(routePath, p.Src, WildcardParam(p.Name), 1)
|
||||
} else {
|
||||
routePath = strings.Replace(routePath, p.Src, Param(p.Name), 1)
|
||||
}
|
||||
}
|
||||
|
||||
return routePath, nil
|
||||
}
|
||||
|
||||
// note: returns nil if not needed, the caller(router) should be check for that before adding that on route's Middleware
|
||||
func convertTmplToHandler(tmpl *macro.Template) context.Handler {
|
||||
needMacroHandler := false
|
||||
|
||||
// check if we have params like: {name:string} or {name} or {anything:path} without else keyword or any functions used inside these params.
|
||||
// 1. if we don't have, then we don't need to add a handler before the main route's handler (as I said, no performance if macro is not really used)
|
||||
// 2. if we don't have any named params then we don't need a handler too.
|
||||
for _, p := range tmpl.Params {
|
||||
if len(p.Funcs) == 0 && (p.Type == ast.ParamTypeString || p.Type == ast.ParamTypePath) && p.ErrCode == http.StatusNotFound {
|
||||
} else {
|
||||
needMacroHandler = true
|
||||
}
|
||||
}
|
||||
|
||||
if !needMacroHandler {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(tmpl macro.Template) context.Handler {
|
||||
return func(ctx context.Context) {
|
||||
for _, p := range tmpl.Params {
|
||||
paramValue := ctx.Params().Get(p.Name)
|
||||
// first, check for type evaluator
|
||||
if !p.TypeEvaluator(paramValue) {
|
||||
ctx.StatusCode(p.ErrCode)
|
||||
ctx.StopExecution()
|
||||
return
|
||||
}
|
||||
|
||||
// then check for all of its functions
|
||||
for _, evalFunc := range p.Funcs {
|
||||
if !evalFunc(paramValue) {
|
||||
ctx.StatusCode(p.ErrCode)
|
||||
ctx.StopExecution()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// if all passed, just continue
|
||||
ctx.Next()
|
||||
}
|
||||
}(*tmpl)
|
||||
|
||||
}
|
||||
27
core/router/macro/LICENSE
Normal file
27
core/router/macro/LICENSE
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Gerasimos Maropoulos nor the name of his
|
||||
username, kataras, may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
122
core/router/macro/interpreter/ast/ast.go
Normal file
122
core/router/macro/interpreter/ast/ast.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ParamType uint8
|
||||
|
||||
const (
|
||||
ParamTypeUnExpected ParamType = iota
|
||||
// /myparam1
|
||||
ParamTypeString
|
||||
// /42
|
||||
ParamTypeInt
|
||||
// /myparam
|
||||
ParamTypeAlphabetical
|
||||
// /main.css
|
||||
ParamTypeFile
|
||||
// /myparam1/myparam2
|
||||
ParamTypePath
|
||||
)
|
||||
|
||||
var paramTypes = map[string]ParamType{
|
||||
"string": ParamTypeString,
|
||||
"int": ParamTypeInt,
|
||||
"alphabetical": ParamTypeAlphabetical,
|
||||
"file": ParamTypeFile,
|
||||
"path": ParamTypePath,
|
||||
// could be named also:
|
||||
// "tail":
|
||||
// "wild"
|
||||
// "wildcard"
|
||||
|
||||
}
|
||||
|
||||
func LookupParamType(ident string) ParamType {
|
||||
if typ, ok := paramTypes[ident]; ok {
|
||||
return typ
|
||||
}
|
||||
return ParamTypeUnExpected
|
||||
}
|
||||
|
||||
type ParamStatement struct {
|
||||
Src string // the original unparsed source, i.e: {id:int range(1,5) else 404}
|
||||
Name string // id
|
||||
Type ParamType // int
|
||||
Funcs []ParamFunc // range
|
||||
ErrorCode int // 404
|
||||
}
|
||||
|
||||
type ParamFuncArg interface{}
|
||||
|
||||
func ParamFuncArgInt64(a ParamFuncArg) (int64, bool) {
|
||||
if v, ok := a.(int64); ok {
|
||||
return v, false
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func ParamFuncArgToInt64(a ParamFuncArg) (int64, error) {
|
||||
switch a.(type) {
|
||||
case int64:
|
||||
return a.(int64), nil
|
||||
case string:
|
||||
return strconv.ParseInt(a.(string), 10, 64)
|
||||
case int:
|
||||
return int64(a.(int)), nil
|
||||
default:
|
||||
return -1, fmt.Errorf("unexpected function argument type: %q", a)
|
||||
}
|
||||
}
|
||||
|
||||
func ParamFuncArgInt(a ParamFuncArg) (int, bool) {
|
||||
if v, ok := a.(int); ok {
|
||||
return v, false
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func ParamFuncArgToInt(a ParamFuncArg) (int, error) {
|
||||
switch a.(type) {
|
||||
case int:
|
||||
return a.(int), nil
|
||||
case string:
|
||||
return strconv.Atoi(a.(string))
|
||||
case int64:
|
||||
return int(a.(int64)), nil
|
||||
default:
|
||||
return -1, fmt.Errorf("unexpected function argument type: %q", a)
|
||||
}
|
||||
}
|
||||
|
||||
func ParamFuncArgString(a ParamFuncArg) (string, bool) {
|
||||
if v, ok := a.(string); ok {
|
||||
return v, false
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func ParamFuncArgToString(a ParamFuncArg) (string, error) {
|
||||
switch a.(type) {
|
||||
case string:
|
||||
return a.(string), nil
|
||||
case int:
|
||||
return strconv.Itoa(a.(int)), nil
|
||||
case int64:
|
||||
return strconv.FormatInt(a.(int64), 10), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unexpected function argument type: %q", a)
|
||||
}
|
||||
}
|
||||
|
||||
// range(1,5)
|
||||
type ParamFunc struct {
|
||||
Name string // range
|
||||
Args []ParamFuncArg // [1,5]
|
||||
}
|
||||
186
core/router/macro/interpreter/lexer/lexer.go
Normal file
186
core/router/macro/interpreter/lexer/lexer.go
Normal file
@@ -0,0 +1,186 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/token"
|
||||
)
|
||||
|
||||
type Lexer struct {
|
||||
input string
|
||||
pos int // current pos in input, current char
|
||||
readPos int // current reading pos in input, after current char
|
||||
ch byte // current char under examination
|
||||
}
|
||||
|
||||
func New(src string) *Lexer {
|
||||
l := &Lexer{
|
||||
input: src,
|
||||
}
|
||||
// step to the first character in order to be ready
|
||||
l.readChar()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Lexer) readChar() {
|
||||
if l.readPos >= len(l.input) {
|
||||
l.ch = 0
|
||||
} else {
|
||||
l.ch = l.input[l.readPos]
|
||||
}
|
||||
l.pos = l.readPos
|
||||
l.readPos += 1
|
||||
}
|
||||
|
||||
const (
|
||||
Begin = '{' // token.LBRACE
|
||||
End = '}' // token.RBRACE
|
||||
)
|
||||
|
||||
func resolveTokenType(ch byte) token.TokenType {
|
||||
switch ch {
|
||||
case Begin:
|
||||
return token.LBRACE
|
||||
case End:
|
||||
return token.RBRACE
|
||||
// Let's keep it simple, no evaluation for logical operators, we are not making a new programming language, keep it simple makis.
|
||||
// ||
|
||||
// case '|':
|
||||
// if l.peekChar() == '|' {
|
||||
// ch := l.ch
|
||||
// l.readChar()
|
||||
// t = token.Token{Type: token.OR, Literal: string(ch) + string(l.ch)}
|
||||
// }
|
||||
// ==
|
||||
case ':':
|
||||
return token.COLON
|
||||
case '(':
|
||||
return token.LPAREN
|
||||
case ')':
|
||||
return token.RPAREN
|
||||
case ',':
|
||||
return token.COMMA
|
||||
// literals
|
||||
case 0:
|
||||
return token.EOF
|
||||
default:
|
||||
return token.IDENT //
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (l *Lexer) NextToken() (t token.Token) {
|
||||
l.skipWhitespace()
|
||||
typ := resolveTokenType(l.ch)
|
||||
t.Type = typ
|
||||
switch typ {
|
||||
case token.EOF:
|
||||
t.Literal = ""
|
||||
case token.IDENT:
|
||||
if isLetter(l.ch) {
|
||||
// letters
|
||||
lit := l.readIdentifier()
|
||||
typ := token.LookupIdent(lit)
|
||||
t = l.newToken(typ, lit)
|
||||
return
|
||||
}
|
||||
if isDigit(l.ch) {
|
||||
// numbers
|
||||
lit := l.readNumber()
|
||||
t = l.newToken(token.INT, lit)
|
||||
return
|
||||
}
|
||||
|
||||
t = l.newTokenRune(token.ILLEGAL, l.ch)
|
||||
default:
|
||||
t = l.newTokenRune(typ, l.ch)
|
||||
}
|
||||
l.readChar() // set the pos to the next
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lexer) NextDynamicToken() (t token.Token) {
|
||||
// calculate anything, even spaces.
|
||||
|
||||
// numbers
|
||||
lit := l.readNumber()
|
||||
if lit != "" {
|
||||
return l.newToken(token.INT, lit)
|
||||
}
|
||||
|
||||
lit = l.readIdentifierFuncArgument()
|
||||
return l.newToken(token.IDENT, lit)
|
||||
}
|
||||
|
||||
// used to skip any illegal token if inside parenthesis, used to be able to set custom regexp inside a func.
|
||||
func (l *Lexer) readIdentifierFuncArgument() string {
|
||||
pos := l.pos
|
||||
for resolveTokenType(l.ch) != token.RPAREN {
|
||||
l.readChar()
|
||||
}
|
||||
|
||||
return l.input[pos:l.pos]
|
||||
}
|
||||
|
||||
// useful when we want to peek but no continue, i.e empty param functions 'even()'
|
||||
func (l *Lexer) PeekNextTokenType() token.TokenType {
|
||||
if len(l.input)-1 > l.pos {
|
||||
ch := l.input[l.pos]
|
||||
return resolveTokenType(ch)
|
||||
}
|
||||
return resolveTokenType(0) // EOF
|
||||
}
|
||||
|
||||
func (l *Lexer) newToken(tokenType token.TokenType, lit string) token.Token {
|
||||
t := token.Token{
|
||||
Type: tokenType,
|
||||
Literal: lit,
|
||||
Start: l.pos,
|
||||
End: l.pos,
|
||||
}
|
||||
// remember, l.pos is the last char
|
||||
// and we want to include both start and end
|
||||
// in order to be easy to the user to see by just marking the expression
|
||||
if l.pos > 1 && len(lit) > 1 {
|
||||
t.End = l.pos - 1
|
||||
t.Start = t.End - len(lit) + 1
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func (l *Lexer) newTokenRune(tokenType token.TokenType, ch byte) token.Token {
|
||||
return l.newToken(tokenType, string(ch))
|
||||
}
|
||||
|
||||
func (l *Lexer) skipWhitespace() {
|
||||
for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
|
||||
l.readChar()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) readIdentifier() string {
|
||||
pos := l.pos
|
||||
for isLetter(l.ch) {
|
||||
l.readChar()
|
||||
}
|
||||
return l.input[pos:l.pos]
|
||||
}
|
||||
|
||||
func isLetter(ch byte) bool {
|
||||
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_'
|
||||
}
|
||||
|
||||
func (l *Lexer) readNumber() string {
|
||||
pos := l.pos
|
||||
for isDigit(l.ch) {
|
||||
l.readChar()
|
||||
}
|
||||
return l.input[pos:l.pos]
|
||||
}
|
||||
|
||||
func isDigit(ch byte) bool {
|
||||
return '0' <= ch && ch <= '9'
|
||||
}
|
||||
58
core/router/macro/interpreter/lexer/lexer_test.go
Normal file
58
core/router/macro/interpreter/lexer/lexer_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/token"
|
||||
)
|
||||
|
||||
func TestNextToken(t *testing.T) {
|
||||
input := `{id:int min(1) max(5) else 404}`
|
||||
|
||||
tests := []struct {
|
||||
expectedType token.TokenType
|
||||
expectedLiteral string
|
||||
}{
|
||||
{token.LBRACE, "{"}, // 0
|
||||
{token.IDENT, "id"}, // 1
|
||||
{token.COLON, ":"}, // 2
|
||||
{token.IDENT, "int"}, // 3
|
||||
{token.IDENT, "min"}, // 4
|
||||
{token.LPAREN, "("}, // 5
|
||||
{token.INT, "1"}, // 6
|
||||
{token.RPAREN, ")"}, // 7
|
||||
{token.IDENT, "max"}, // 8
|
||||
{token.LPAREN, "("}, // 9
|
||||
{token.INT, "5"}, // 10
|
||||
{token.RPAREN, ")"}, // 11
|
||||
{token.ELSE, "else"}, // 12
|
||||
{token.INT, "404"}, // 13
|
||||
{token.RBRACE, "}"}, // 14
|
||||
}
|
||||
|
||||
l := New(input)
|
||||
|
||||
for i, tt := range tests {
|
||||
tok := l.NextToken()
|
||||
|
||||
if tok.Type != tt.expectedType {
|
||||
t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q",
|
||||
i, tt.expectedType, tok.Type)
|
||||
}
|
||||
|
||||
if tok.Literal != tt.expectedLiteral {
|
||||
t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q",
|
||||
i, tt.expectedLiteral, tok.Literal)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// EMEINA STO:
|
||||
// 30/232 selida apto making a interpeter in Go.
|
||||
// den ekana to skipWhitespaces giati skeftomai
|
||||
// an borei na to xreiastw 9a dw aurio.
|
||||
180
core/router/macro/interpreter/parser/parser.go
Normal file
180
core/router/macro/interpreter/parser/parser.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/lexer"
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/token"
|
||||
)
|
||||
|
||||
func Parse(fullpath string) ([]*ast.ParamStatement, error) {
|
||||
pathParts := strings.SplitN(fullpath, "/", -1)
|
||||
p := new(ParamParser)
|
||||
statements := make([]*ast.ParamStatement, 0)
|
||||
for i, s := range pathParts {
|
||||
if s == "" { // if starts with /
|
||||
continue
|
||||
}
|
||||
|
||||
// if it's not a named path parameter of the new syntax then continue to the next
|
||||
if s[0] != lexer.Begin || s[len(s)-1] != lexer.End {
|
||||
continue
|
||||
}
|
||||
|
||||
p.Reset(s)
|
||||
stmt, err := p.Parse()
|
||||
if err != nil {
|
||||
// exit on first error
|
||||
return nil, err
|
||||
}
|
||||
// if we have param type path but it's not the last path part
|
||||
if stmt.Type == ast.ParamTypePath && i < len(pathParts)-1 {
|
||||
return nil, fmt.Errorf("param type 'path' should be lived only inside the last path segment, but was inside: %s", s)
|
||||
}
|
||||
|
||||
statements = append(statements, stmt)
|
||||
}
|
||||
|
||||
return statements, nil
|
||||
}
|
||||
|
||||
type ParamParser struct {
|
||||
src string
|
||||
errors []string
|
||||
}
|
||||
|
||||
func NewParamParser(src string) *ParamParser {
|
||||
p := new(ParamParser)
|
||||
p.Reset(src)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *ParamParser) Reset(src string) {
|
||||
p.src = src
|
||||
|
||||
p.errors = []string{}
|
||||
}
|
||||
|
||||
func (p *ParamParser) appendErr(format string, a ...interface{}) {
|
||||
p.errors = append(p.errors, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
const DefaultParamErrorCode = 404
|
||||
const DefaultParamType = ast.ParamTypeString
|
||||
|
||||
func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) {
|
||||
if t.Type == token.INT {
|
||||
return ast.ParamFuncArgToInt(t.Literal)
|
||||
}
|
||||
return t.Literal, nil
|
||||
}
|
||||
|
||||
func (p ParamParser) Error() error {
|
||||
if len(p.errors) > 0 {
|
||||
return fmt.Errorf(strings.Join(p.errors, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ParamParser) Parse() (*ast.ParamStatement, error) {
|
||||
l := lexer.New(p.src)
|
||||
|
||||
stmt := &ast.ParamStatement{
|
||||
ErrorCode: DefaultParamErrorCode,
|
||||
Type: DefaultParamType,
|
||||
Src: p.src,
|
||||
}
|
||||
|
||||
lastParamFunc := ast.ParamFunc{}
|
||||
|
||||
for {
|
||||
t := l.NextToken()
|
||||
if t.Type == token.EOF {
|
||||
if stmt.Name == "" {
|
||||
p.appendErr("[1:] parameter name is missing")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
switch t.Type {
|
||||
case token.LBRACE:
|
||||
// name, alphabetical and _, param names are not allowed to contain any number.
|
||||
nextTok := l.NextToken()
|
||||
stmt.Name = nextTok.Literal
|
||||
case token.COLON:
|
||||
// type
|
||||
nextTok := l.NextToken()
|
||||
paramType := ast.LookupParamType(nextTok.Literal)
|
||||
if paramType == ast.ParamTypeUnExpected {
|
||||
p.appendErr("[%d:%d] unexpected parameter type: %s", t.Start, t.End, nextTok.Literal)
|
||||
}
|
||||
stmt.Type = paramType
|
||||
// param func
|
||||
case token.IDENT:
|
||||
lastParamFunc.Name = t.Literal
|
||||
case token.LPAREN:
|
||||
// param function without arguments ()
|
||||
if l.PeekNextTokenType() == token.RPAREN {
|
||||
// do nothing, just continue to the RPAREN
|
||||
continue
|
||||
}
|
||||
|
||||
argValTok := l.NextDynamicToken() // catch anything from "(" and forward, until ")", because we need to
|
||||
// be able to use regex expression as a macro type's func argument too.
|
||||
argVal, err := parseParamFuncArg(argValTok)
|
||||
if err != nil {
|
||||
p.appendErr("[%d:%d] expected param func argument to be a string or number but got %s", t.Start, t.End, argValTok.Literal)
|
||||
continue
|
||||
}
|
||||
|
||||
// fmt.Printf("argValTok: %#v\n", argValTok)
|
||||
// fmt.Printf("argVal: %#v\n", argVal)
|
||||
lastParamFunc.Args = append(lastParamFunc.Args, argVal)
|
||||
|
||||
case token.COMMA:
|
||||
argValTok := l.NextToken()
|
||||
argVal, err := parseParamFuncArg(argValTok)
|
||||
if err != nil {
|
||||
p.appendErr("[%d:%d] expected param func argument to be a string or number type but got %s", t.Start, t.End, argValTok.Literal)
|
||||
continue
|
||||
}
|
||||
|
||||
lastParamFunc.Args = append(lastParamFunc.Args, argVal)
|
||||
case token.RPAREN:
|
||||
stmt.Funcs = append(stmt.Funcs, lastParamFunc)
|
||||
lastParamFunc = ast.ParamFunc{} // reset
|
||||
case token.ELSE:
|
||||
errCodeTok := l.NextToken()
|
||||
if errCodeTok.Type != token.INT {
|
||||
p.appendErr("[%d:%d] expected error code to be an integer but got %s", t.Start, t.End, errCodeTok.Literal)
|
||||
continue
|
||||
}
|
||||
errCode, err := strconv.Atoi(errCodeTok.Literal)
|
||||
if err != nil {
|
||||
// this is a bug on lexer if throws because we already check for token.INT
|
||||
p.appendErr("[%d:%d] unexpected lexer error while trying to convert error code to an integer, %s", t.Start, t.End, err.Error())
|
||||
continue
|
||||
}
|
||||
stmt.ErrorCode = errCode
|
||||
case token.RBRACE:
|
||||
// check if } but not {
|
||||
if stmt.Name == "" {
|
||||
p.appendErr("[%d:%d] illegal token: }, forgot '{' ?", t.Start, t.End)
|
||||
}
|
||||
break
|
||||
case token.ILLEGAL:
|
||||
p.appendErr("[%d:%d] illegal token: %s", t.Start, t.End, t.Literal)
|
||||
default:
|
||||
p.appendErr("[%d:%d] unexpected token type: %q with value %s", t.Start, t.End, t.Type, t.Literal)
|
||||
}
|
||||
}
|
||||
|
||||
return stmt, p.Error()
|
||||
}
|
||||
258
core/router/macro/interpreter/parser/parser_test.go
Normal file
258
core/router/macro/interpreter/parser/parser_test.go
Normal file
@@ -0,0 +1,258 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
)
|
||||
|
||||
func TestParseParamError(t *testing.T) {
|
||||
// fail
|
||||
illegalChar := '$'
|
||||
|
||||
input := "{id" + string(illegalChar) + "int range(1,5) else 404}"
|
||||
p := NewParamParser(input)
|
||||
|
||||
_, err := p.Parse()
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("expecting not empty error on input '%s'", input)
|
||||
}
|
||||
|
||||
illIdx := strings.IndexRune(input, illegalChar)
|
||||
expectedErr := fmt.Sprintf("[%d:%d] illegal token: %s", illIdx, illIdx, "$")
|
||||
if got := err.Error(); got != expectedErr {
|
||||
t.Fatalf("expecting error to be '%s' but got: %s", expectedErr, got)
|
||||
}
|
||||
//
|
||||
|
||||
// success
|
||||
input2 := "{id:int range(1,5) else 404}"
|
||||
p.Reset(input2)
|
||||
_, err = p.Parse()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expecting empty error on input '%s', but got: %s", input2, err.Error())
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
func TestParseParam(t *testing.T) {
|
||||
tests := []struct {
|
||||
valid bool
|
||||
expectedStatement ast.ParamStatement
|
||||
}{
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{id:int min(1) max(5) else 404}",
|
||||
Name: "id",
|
||||
Type: ast.ParamTypeInt,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "min",
|
||||
Args: []ast.ParamFuncArg{1}},
|
||||
{
|
||||
Name: "max",
|
||||
Args: []ast.ParamFuncArg{5}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
}}, // 0
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{id:int range(1,5)}",
|
||||
Name: "id",
|
||||
Type: ast.ParamTypeInt,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "range",
|
||||
Args: []ast.ParamFuncArg{1, 5}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
}}, // 1
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{file:path contains(.)}",
|
||||
Name: "file",
|
||||
Type: ast.ParamTypePath,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "contains",
|
||||
Args: []ast.ParamFuncArg{"."}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
}}, // 2
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{username:alphabetical}",
|
||||
Name: "username",
|
||||
Type: ast.ParamTypeAlphabetical,
|
||||
ErrorCode: 404,
|
||||
}}, // 3
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{myparam}",
|
||||
Name: "myparam",
|
||||
Type: ast.ParamTypeString,
|
||||
ErrorCode: 404,
|
||||
}}, // 4
|
||||
{false,
|
||||
ast.ParamStatement{
|
||||
Src: "{myparam_:thisianunexpected}",
|
||||
Name: "myparam_",
|
||||
Type: ast.ParamTypeUnExpected,
|
||||
ErrorCode: 404,
|
||||
}}, // 5
|
||||
{false, // false because it will give an error of unexpeced token type with value 2
|
||||
ast.ParamStatement{
|
||||
Src: "{myparam2}",
|
||||
Name: "myparam", // expected "myparam" because we don't allow integers to the parameter names.
|
||||
Type: ast.ParamTypeString,
|
||||
ErrorCode: 404,
|
||||
}}, // 6
|
||||
{true,
|
||||
ast.ParamStatement{
|
||||
Src: "{id:int even()}", // test param funcs without any arguments (LPAREN peek for RPAREN)
|
||||
Name: "id",
|
||||
Type: ast.ParamTypeInt,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "even"},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
}}, // 7
|
||||
|
||||
}
|
||||
var p *ParamParser = new(ParamParser)
|
||||
for i, tt := range tests {
|
||||
p.Reset(tt.expectedStatement.Src)
|
||||
resultStmt, err := p.Parse()
|
||||
|
||||
if tt.valid && err != nil {
|
||||
t.Fatalf("tests[%d] - error %s", i, err.Error())
|
||||
} else if !tt.valid && err == nil {
|
||||
t.Fatalf("tests[%d] - expected to be a failure", i)
|
||||
}
|
||||
|
||||
if resultStmt != nil { // is valid here
|
||||
if !reflect.DeepEqual(tt.expectedStatement, *resultStmt) {
|
||||
t.Fatalf("tests[%d] - wrong statement, expected and result differs. Details:\n%#v\n%#v", i, tt.expectedStatement, *resultStmt)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
valid bool
|
||||
expectedStatements []ast.ParamStatement
|
||||
}{
|
||||
{"/api/users/{id:int min(1) max(5) else 404}", true,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{id:int min(1) max(5) else 404}",
|
||||
Name: "id",
|
||||
Type: ast.ParamTypeInt,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "min",
|
||||
Args: []ast.ParamFuncArg{1}},
|
||||
{
|
||||
Name: "max",
|
||||
Args: []ast.ParamFuncArg{5}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 0
|
||||
{"/admin/{id:int range(1,5)}", true,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{id:int range(1,5)}",
|
||||
Name: "id",
|
||||
Type: ast.ParamTypeInt,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "range",
|
||||
Args: []ast.ParamFuncArg{1, 5}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 1
|
||||
{"/files/{file:path contains(.)}", true,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{file:path contains(.)}",
|
||||
Name: "file",
|
||||
Type: ast.ParamTypePath,
|
||||
Funcs: []ast.ParamFunc{
|
||||
{
|
||||
Name: "contains",
|
||||
Args: []ast.ParamFuncArg{"."}},
|
||||
},
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 2
|
||||
{"/profile/{username:alphabetical}", true,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{username:alphabetical}",
|
||||
Name: "username",
|
||||
Type: ast.ParamTypeAlphabetical,
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 3
|
||||
{"/something/here/{myparam}", true,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{myparam}",
|
||||
Name: "myparam",
|
||||
Type: ast.ParamTypeString,
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 4
|
||||
{"/unexpected/{myparam_:thisianunexpected}", false,
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{myparam_:thisianunexpected}",
|
||||
Name: "myparam_",
|
||||
Type: ast.ParamTypeUnExpected,
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 5
|
||||
{"/p2/{myparam2}", false, // false because it will give an error of unexpeced token type with value 2
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{myparam2}",
|
||||
Name: "myparam", // expected "myparam" because we don't allow integers to the parameter names.
|
||||
Type: ast.ParamTypeString,
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 6
|
||||
{"/assets/{file:path}/invalid", false, // path should be in the end segment
|
||||
[]ast.ParamStatement{{
|
||||
Src: "{file:path}",
|
||||
Name: "file",
|
||||
Type: ast.ParamTypePath,
|
||||
ErrorCode: 404,
|
||||
},
|
||||
}}, // 7
|
||||
}
|
||||
for i, tt := range tests {
|
||||
statements, err := Parse(tt.path)
|
||||
|
||||
if tt.valid && err != nil {
|
||||
t.Fatalf("tests[%d] - error %s", i, err.Error())
|
||||
} else if !tt.valid && err == nil {
|
||||
t.Fatalf("tests[%d] - expected to be a failure", i)
|
||||
}
|
||||
for j := range statements {
|
||||
for l := range tt.expectedStatements {
|
||||
if !reflect.DeepEqual(tt.expectedStatements[l], *statements[j]) {
|
||||
t.Fatalf("tests[%d] - wrong statements, expected and result differs. Details:\n%#v\n%#v", i, tt.expectedStatements[l], *statements[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
53
core/router/macro/interpreter/token/token.go
Normal file
53
core/router/macro/interpreter/token/token.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package token
|
||||
|
||||
type TokenType int
|
||||
|
||||
type Token struct {
|
||||
Type TokenType
|
||||
Literal string
|
||||
Start int // including the first char
|
||||
End int // including the last char
|
||||
}
|
||||
|
||||
// /about/{fullname:alphabetical}
|
||||
// /profile/{anySpecialName:string}
|
||||
// {id:int range(1,5) else 404}
|
||||
// /admin/{id:int eq(1) else 402}
|
||||
// /file/{filepath:file else 405}
|
||||
const (
|
||||
EOF = iota // 0
|
||||
ILLEGAL
|
||||
|
||||
// Identifiers + literals
|
||||
LBRACE // {
|
||||
RBRACE // }
|
||||
// PARAM_IDENTIFIER // id
|
||||
COLON // :
|
||||
LPAREN // (
|
||||
RPAREN // )
|
||||
// PARAM_FUNC_ARG // 1
|
||||
COMMA
|
||||
IDENT // string or keyword
|
||||
// Keywords
|
||||
keywords_start
|
||||
ELSE // else
|
||||
keywords_end
|
||||
INT // 42
|
||||
)
|
||||
|
||||
const eof rune = 0
|
||||
|
||||
var keywords = map[string]TokenType{
|
||||
"else": ELSE,
|
||||
}
|
||||
|
||||
func LookupIdent(ident string) TokenType {
|
||||
if tok, ok := keywords[ident]; ok {
|
||||
return tok
|
||||
}
|
||||
return IDENT
|
||||
}
|
||||
229
core/router/macro/macro.go
Normal file
229
core/router/macro/macro.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package macro
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"unicode"
|
||||
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
)
|
||||
|
||||
// final evaluator signature for both param types and param funcs
|
||||
type EvaluatorFunc func(paramValue string) bool
|
||||
|
||||
func NewEvaluatorFromRegexp(expr string) (EvaluatorFunc, error) {
|
||||
if expr == "" {
|
||||
return nil, fmt.Errorf("empty regex expression")
|
||||
}
|
||||
|
||||
// add the last $ if missing (and not wildcard(?))
|
||||
if i := expr[len(expr)-1]; i != '$' && i != '*' {
|
||||
expr += "$"
|
||||
}
|
||||
|
||||
r, err := regexp.Compile(expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.MatchString, nil
|
||||
}
|
||||
|
||||
func MustNewEvaluatorFromRegexp(expr string) EvaluatorFunc {
|
||||
r, err := NewEvaluatorFromRegexp(expr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
var (
|
||||
goodParamFuncReturnType = reflect.TypeOf(func(string) bool { return false })
|
||||
goodParamFuncReturnType2 = reflect.TypeOf(EvaluatorFunc(func(string) bool { return false }))
|
||||
)
|
||||
|
||||
func goodParamFunc(typ reflect.Type) bool {
|
||||
// should be a func
|
||||
// which returns a func(string) bool
|
||||
if typ.Kind() == reflect.Func {
|
||||
if typ.NumOut() == 1 {
|
||||
typOut := typ.Out(0)
|
||||
if typOut == goodParamFuncReturnType || typOut == goodParamFuncReturnType2 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// goodParamFuncName reports whether the function name is a valid identifier.
|
||||
func goodParamFuncName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
// valid names are only letters and _
|
||||
for _, r := range name {
|
||||
switch {
|
||||
case r == '_':
|
||||
case !unicode.IsLetter(r):
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// the convertBuilderFunc return value is generating at boot time.
|
||||
// convertFunc converts an interface to a valid full param function.
|
||||
func convertBuilderFunc(fn interface{}) ParamEvaluatorBuilder {
|
||||
|
||||
typFn := reflect.TypeOf(fn)
|
||||
if !goodParamFunc(typFn) {
|
||||
return nil
|
||||
}
|
||||
|
||||
numFields := typFn.NumIn()
|
||||
|
||||
return func(args []ast.ParamFuncArg) EvaluatorFunc {
|
||||
if len(args) != numFields {
|
||||
// no variadics support, for now.
|
||||
panic("args should be the same len as numFields")
|
||||
}
|
||||
var argValues []reflect.Value
|
||||
for i := 0; i < numFields; i++ {
|
||||
field := typFn.In(i)
|
||||
arg := args[i]
|
||||
|
||||
if field.Kind() != reflect.TypeOf(arg).Kind() {
|
||||
panic("fields should have the same type")
|
||||
}
|
||||
|
||||
argValues = append(argValues, reflect.ValueOf(arg))
|
||||
}
|
||||
|
||||
evalFn := reflect.ValueOf(fn).Call(argValues)[0].Interface()
|
||||
|
||||
var evaluator EvaluatorFunc
|
||||
// check for typed and not typed
|
||||
if _v, ok := evalFn.(EvaluatorFunc); ok {
|
||||
evaluator = _v
|
||||
} else if _v, ok = evalFn.(func(string) bool); ok {
|
||||
evaluator = _v
|
||||
}
|
||||
return func(paramValue string) bool {
|
||||
return evaluator(paramValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
Macro struct {
|
||||
Evaluator EvaluatorFunc
|
||||
funcs []ParamFunc
|
||||
}
|
||||
|
||||
ParamEvaluatorBuilder func([]ast.ParamFuncArg) EvaluatorFunc
|
||||
|
||||
ParamFunc struct {
|
||||
Name string
|
||||
Func ParamEvaluatorBuilder
|
||||
}
|
||||
)
|
||||
|
||||
func newMacro(evaluator EvaluatorFunc) *Macro {
|
||||
return &Macro{Evaluator: evaluator}
|
||||
}
|
||||
|
||||
// at boot time, per param
|
||||
func (m *Macro) RegisterFunc(funcName string, fn interface{}) {
|
||||
fullFn := convertBuilderFunc(fn)
|
||||
m.registerFunc(funcName, fullFn)
|
||||
}
|
||||
|
||||
func (m *Macro) registerFunc(funcName string, fullFn ParamEvaluatorBuilder) {
|
||||
if !goodParamFuncName(funcName) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, fn := range m.funcs {
|
||||
if fn.Name == funcName {
|
||||
fn.Func = fullFn
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
m.funcs = append(m.funcs, ParamFunc{
|
||||
Name: funcName,
|
||||
Func: fullFn,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Macro) getFunc(funcName string) ParamEvaluatorBuilder {
|
||||
for _, fn := range m.funcs {
|
||||
if fn.Name == funcName {
|
||||
if fn.Func == nil {
|
||||
continue
|
||||
}
|
||||
return fn.Func
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MacroMap struct {
|
||||
// string type
|
||||
// anything
|
||||
String *Macro
|
||||
// int type
|
||||
// only numbers (0-9)
|
||||
Int *Macro
|
||||
// alphabetical/letter type
|
||||
// letters only (upper or lowercase)
|
||||
Alphabetical *Macro
|
||||
// file type
|
||||
// letters (upper or lowercase)
|
||||
// numbers (0-9)
|
||||
// underscore (_)
|
||||
// dash (-)
|
||||
// point (.)
|
||||
// no spaces! or other character
|
||||
File *Macro
|
||||
// path type
|
||||
// anything, should be the last part
|
||||
Path *Macro
|
||||
}
|
||||
|
||||
func NewMacroMap() *MacroMap {
|
||||
return &MacroMap{
|
||||
// it allows everything, so no need for a regexp here.
|
||||
String: newMacro(func(string) bool { return true }),
|
||||
Int: newMacro(MustNewEvaluatorFromRegexp("^[0-9]+$")),
|
||||
Alphabetical: newMacro(MustNewEvaluatorFromRegexp("^[a-zA-Z ]+$")),
|
||||
File: newMacro(MustNewEvaluatorFromRegexp("^[a-zA-Z0-9_.-]*$")),
|
||||
// it allows everything, we have String and Path as different
|
||||
// types because I want to give the opportunity to the user
|
||||
// to organise the macro functions based on wildcard or single dynamic named path parameter.
|
||||
// Should be the last.
|
||||
Path: newMacro(func(string) bool { return true }),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MacroMap) Lookup(typ ast.ParamType) *Macro {
|
||||
switch typ {
|
||||
case ast.ParamTypeInt:
|
||||
return m.Int
|
||||
case ast.ParamTypeAlphabetical:
|
||||
return m.Alphabetical
|
||||
case ast.ParamTypeFile:
|
||||
return m.File
|
||||
case ast.ParamTypePath:
|
||||
return m.Path
|
||||
default:
|
||||
return m.String
|
||||
}
|
||||
}
|
||||
190
core/router/macro/macro_test.go
Normal file
190
core/router/macro/macro_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package macro
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Most important tests to look:
|
||||
// ../parser/parser_test.go
|
||||
// ../lexer/lexer_test.go
|
||||
|
||||
func TestGoodParamFunc(t *testing.T) {
|
||||
good1 := func(min int, max int) func(string) bool {
|
||||
return func(paramValue string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
good2 := func(min int, max int) func(string) bool {
|
||||
return func(paramValue string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
notgood1 := func(min int, max int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if !goodParamFunc(reflect.TypeOf(good1)) {
|
||||
t.Fatalf("expected good1 func to be good but it's not")
|
||||
}
|
||||
|
||||
if !goodParamFunc(reflect.TypeOf(good2)) {
|
||||
t.Fatalf("expected good2 func to be good but it's not")
|
||||
}
|
||||
|
||||
if goodParamFunc(reflect.TypeOf(notgood1)) {
|
||||
t.Fatalf("expected notgood1 func to be the worst")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoodParamFuncName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
good bool
|
||||
}{
|
||||
{"range", true},
|
||||
{"_range", true},
|
||||
{"range_", true},
|
||||
{"r_ange", true},
|
||||
// numbers or other symbols are invalid.
|
||||
{"range1", false},
|
||||
{"2range", false},
|
||||
{"r@nge", false},
|
||||
{"rang3", false},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
isGood := goodParamFuncName(tt.name)
|
||||
if tt.good && !isGood {
|
||||
t.Fatalf("tests[%d] - expecting valid name but got invalid for name %s", i, tt.name)
|
||||
} else if !tt.good && isGood {
|
||||
t.Fatalf("tests[%d] - expecting invalid name but got valid for name %s", i, tt.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testEvaluatorRaw(macroEvaluator *Macro, input string, pass bool, i int, t *testing.T) {
|
||||
if got := macroEvaluator.Evaluator(input); pass != got {
|
||||
t.Fatalf("tests[%d] - expecting %v but got %v", i, pass, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringEvaluatorRaw(t *testing.T) {
|
||||
f := NewMacroMap()
|
||||
|
||||
tests := []struct {
|
||||
pass bool
|
||||
input string
|
||||
}{
|
||||
{true, "astring"}, // 0
|
||||
{true, "astringwith_numb3rS_and_symbol$"}, // 1
|
||||
{true, "32321"}, // 2
|
||||
{true, "main.css"}, // 3
|
||||
{true, "/assets/main.css"}, // 4
|
||||
// false never
|
||||
} // 0
|
||||
|
||||
for i, tt := range tests {
|
||||
testEvaluatorRaw(f.String, tt.input, tt.pass, i, t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntEvaluatorRaw(t *testing.T) {
|
||||
f := NewMacroMap()
|
||||
|
||||
tests := []struct {
|
||||
pass bool
|
||||
input string
|
||||
}{
|
||||
{false, "astring"}, // 0
|
||||
{false, "astringwith_numb3rS_and_symbol$"}, // 1
|
||||
{true, "32321"}, // 2
|
||||
{false, "main.css"}, // 3
|
||||
{false, "/assets/main.css"}, // 4
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
testEvaluatorRaw(f.Int, tt.input, tt.pass, i, t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlphabeticalEvaluatorRaw(t *testing.T) {
|
||||
f := NewMacroMap()
|
||||
|
||||
tests := []struct {
|
||||
pass bool
|
||||
input string
|
||||
}{
|
||||
{true, "astring"}, // 0
|
||||
{false, "astringwith_numb3rS_and_symbol$"}, // 1
|
||||
{false, "32321"}, // 2
|
||||
{false, "main.css"}, // 3
|
||||
{false, "/assets/main.css"}, // 4
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
testEvaluatorRaw(f.Alphabetical, tt.input, tt.pass, i, t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileEvaluatorRaw(t *testing.T) {
|
||||
f := NewMacroMap()
|
||||
|
||||
tests := []struct {
|
||||
pass bool
|
||||
input string
|
||||
}{
|
||||
{true, "astring"}, // 0
|
||||
{false, "astringwith_numb3rS_and_symbol$"}, // 1
|
||||
{true, "32321"}, // 2
|
||||
{true, "main.css"}, // 3
|
||||
{false, "/assets/main.css"}, // 4
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
testEvaluatorRaw(f.File, tt.input, tt.pass, i, t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathEvaluatorRaw(t *testing.T) {
|
||||
f := NewMacroMap()
|
||||
|
||||
pathTests := []struct {
|
||||
pass bool
|
||||
input string
|
||||
}{
|
||||
{true, "astring"}, // 0
|
||||
{true, "astringwith_numb3rS_and_symbol$"}, // 1
|
||||
{true, "32321"}, // 2
|
||||
{true, "main.css"}, // 3
|
||||
{true, "/assets/main.css"}, // 4
|
||||
{true, "disk/assets/main.css"}, // 5
|
||||
}
|
||||
|
||||
for i, tt := range pathTests {
|
||||
testEvaluatorRaw(f.Path, tt.input, tt.pass, i, t)
|
||||
}
|
||||
}
|
||||
|
||||
// func TestMapRegisterFunc(t *testing.T) {
|
||||
// m := NewMacroMap()
|
||||
// m.String.RegisterFunc("prefix", func(prefix string) EvaluatorFunc {
|
||||
// return func(paramValue string) bool {
|
||||
// return strings.HasPrefix(paramValue, prefix)
|
||||
// }
|
||||
// })
|
||||
|
||||
// p, err := Parse("/user/@kataras")
|
||||
// if err != nil {
|
||||
// t.Fatalf(err)
|
||||
// }
|
||||
|
||||
// // p.Params = append(p.)
|
||||
|
||||
// testEvaluatorRaw(m.String, p.Src, false, 0, t)
|
||||
// }
|
||||
67
core/router/macro/template.go
Normal file
67
core/router/macro/template.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package macro
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/ast"
|
||||
"github.com/kataras/iris/core/router/macro/interpreter/parser"
|
||||
)
|
||||
|
||||
type Template struct {
|
||||
// Src is the original template given by the client
|
||||
Src string
|
||||
Params []TemplateParam
|
||||
}
|
||||
|
||||
type TemplateParam struct {
|
||||
Src string // the unparsed param'false source
|
||||
// Type is not useful anywhere here but maybe
|
||||
// it's useful on host to decide how to convert the path template to specific router's syntax
|
||||
Type ast.ParamType
|
||||
Name string
|
||||
ErrCode int
|
||||
TypeEvaluator EvaluatorFunc
|
||||
Funcs []EvaluatorFunc
|
||||
}
|
||||
|
||||
func Parse(src string, macros *MacroMap) (*Template, error) {
|
||||
params, err := parser.Parse(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := new(Template)
|
||||
t.Src = src
|
||||
|
||||
for _, p := range params {
|
||||
funcMap := macros.Lookup(p.Type)
|
||||
typEval := funcMap.Evaluator
|
||||
|
||||
tmplParam := TemplateParam{
|
||||
Src: p.Src,
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
ErrCode: p.ErrorCode,
|
||||
TypeEvaluator: typEval,
|
||||
}
|
||||
for _, paramfn := range p.Funcs {
|
||||
tmplFn := funcMap.getFunc(paramfn.Name)
|
||||
if tmplFn == nil { // if not find on this type, check for String's which is for global funcs too
|
||||
tmplFn = macros.String.getFunc(paramfn.Name)
|
||||
if tmplFn == nil { // if not found then just skip this param
|
||||
continue
|
||||
}
|
||||
}
|
||||
evalFn := tmplFn(paramfn.Args)
|
||||
if evalFn == nil {
|
||||
continue
|
||||
}
|
||||
tmplParam.Funcs = append(tmplParam.Funcs, evalFn)
|
||||
}
|
||||
|
||||
t.Params = append(t.Params, tmplParam)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
596
core/router/mime.go
Normal file
596
core/router/mime.go
Normal file
@@ -0,0 +1,596 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var types = map[string]string{
|
||||
".3dm": "x-world/x-3dmf",
|
||||
".3dmf": "x-world/x-3dmf",
|
||||
".7z": "application/x-7z-compressed",
|
||||
".a": "application/octet-stream",
|
||||
".aab": "application/x-authorware-bin",
|
||||
".aam": "application/x-authorware-map",
|
||||
".aas": "application/x-authorware-seg",
|
||||
".abc": "text/vndabc",
|
||||
".ace": "application/x-ace-compressed",
|
||||
".acgi": "text/html",
|
||||
".afl": "video/animaflex",
|
||||
".ai": "application/postscript",
|
||||
".aif": "audio/aiff",
|
||||
".aifc": "audio/aiff",
|
||||
".aiff": "audio/aiff",
|
||||
".aim": "application/x-aim",
|
||||
".aip": "text/x-audiosoft-intra",
|
||||
".alz": "application/x-alz-compressed",
|
||||
".ani": "application/x-navi-animation",
|
||||
".aos": "application/x-nokia-9000-communicator-add-on-software",
|
||||
".aps": "application/mime",
|
||||
".apk": "application/vnd.android.package-archive",
|
||||
".arc": "application/x-arc-compressed",
|
||||
".arj": "application/arj",
|
||||
".art": "image/x-jg",
|
||||
".asf": "video/x-ms-asf",
|
||||
".asm": "text/x-asm",
|
||||
".asp": "text/asp",
|
||||
".asx": "application/x-mplayer2",
|
||||
".au": "audio/basic",
|
||||
".avi": "video/x-msvideo",
|
||||
".avs": "video/avs-video",
|
||||
".bcpio": "application/x-bcpio",
|
||||
".bin": "application/mac-binary",
|
||||
".bmp": "image/bmp",
|
||||
".boo": "application/book",
|
||||
".book": "application/book",
|
||||
".boz": "application/x-bzip2",
|
||||
".bsh": "application/x-bsh",
|
||||
".bz2": "application/x-bzip2",
|
||||
".bz": "application/x-bzip",
|
||||
".c++": "text/plain",
|
||||
".c": "text/x-c",
|
||||
".cab": "application/vnd.ms-cab-compressed",
|
||||
".cat": "application/vndms-pkiseccat",
|
||||
".cc": "text/x-c",
|
||||
".ccad": "application/clariscad",
|
||||
".cco": "application/x-cocoa",
|
||||
".cdf": "application/cdf",
|
||||
".cer": "application/pkix-cert",
|
||||
".cha": "application/x-chat",
|
||||
".chat": "application/x-chat",
|
||||
".chrt": "application/vnd.kde.kchart",
|
||||
".class": "application/java",
|
||||
".com": "text/plain",
|
||||
".conf": "text/plain",
|
||||
".cpio": "application/x-cpio",
|
||||
".cpp": "text/x-c",
|
||||
".cpt": "application/mac-compactpro",
|
||||
".crl": "application/pkcs-crl",
|
||||
".crt": "application/pkix-cert",
|
||||
".crx": "application/x-chrome-extension",
|
||||
".csh": "text/x-scriptcsh",
|
||||
".css": "text/css",
|
||||
".csv": "text/csv",
|
||||
".cxx": "text/plain",
|
||||
".dar": "application/x-dar",
|
||||
".dcr": "application/x-director",
|
||||
".deb": "application/x-debian-package",
|
||||
".deepv": "application/x-deepv",
|
||||
".def": "text/plain",
|
||||
".der": "application/x-x509-ca-cert",
|
||||
".dif": "video/x-dv",
|
||||
".dir": "application/x-director",
|
||||
".divx": "video/divx",
|
||||
".dl": "video/dl",
|
||||
".dmg": "application/x-apple-diskimage",
|
||||
".doc": "application/msword",
|
||||
".dot": "application/msword",
|
||||
".dp": "application/commonground",
|
||||
".drw": "application/drafting",
|
||||
".dump": "application/octet-stream",
|
||||
".dv": "video/x-dv",
|
||||
".dvi": "application/x-dvi",
|
||||
".dwf": "drawing/x-dwf=(old)",
|
||||
".dwg": "application/acad",
|
||||
".dxf": "application/dxf",
|
||||
".dxr": "application/x-director",
|
||||
".el": "text/x-scriptelisp",
|
||||
".elc": "application/x-bytecodeelisp=(compiled=elisp)",
|
||||
".eml": "message/rfc822",
|
||||
".env": "application/x-envoy",
|
||||
".eps": "application/postscript",
|
||||
".es": "application/x-esrehber",
|
||||
".etx": "text/x-setext",
|
||||
".evy": "application/envoy",
|
||||
".exe": "application/octet-stream",
|
||||
".f77": "text/x-fortran",
|
||||
".f90": "text/x-fortran",
|
||||
".f": "text/x-fortran",
|
||||
".fdf": "application/vndfdf",
|
||||
".fif": "application/fractals",
|
||||
".fli": "video/fli",
|
||||
".flo": "image/florian",
|
||||
".flv": "video/x-flv",
|
||||
".flx": "text/vndfmiflexstor",
|
||||
".fmf": "video/x-atomic3d-feature",
|
||||
".for": "text/x-fortran",
|
||||
".fpx": "image/vndfpx",
|
||||
".frl": "application/freeloader",
|
||||
".funk": "audio/make",
|
||||
".g3": "image/g3fax",
|
||||
".g": "text/plain",
|
||||
".gif": "image/gif",
|
||||
".gl": "video/gl",
|
||||
".gsd": "audio/x-gsm",
|
||||
".gsm": "audio/x-gsm",
|
||||
".gsp": "application/x-gsp",
|
||||
".gss": "application/x-gss",
|
||||
".gtar": "application/x-gtar",
|
||||
".gz": "application/x-compressed",
|
||||
".gzip": "application/x-gzip",
|
||||
".h": "text/x-h",
|
||||
".hdf": "application/x-hdf",
|
||||
".help": "application/x-helpfile",
|
||||
".hgl": "application/vndhp-hpgl",
|
||||
".hh": "text/x-h",
|
||||
".hlb": "text/x-script",
|
||||
".hlp": "application/hlp",
|
||||
".hpg": "application/vndhp-hpgl",
|
||||
".hpgl": "application/vndhp-hpgl",
|
||||
".hqx": "application/binhex",
|
||||
".hta": "application/hta",
|
||||
".htc": "text/x-component",
|
||||
".htm": "text/html",
|
||||
".html": "text/html",
|
||||
".htmls": "text/html",
|
||||
".htt": "text/webviewhtml",
|
||||
".htx": "text/html",
|
||||
".ice": "x-conference/x-cooltalk",
|
||||
".ico": "image/x-icon",
|
||||
".ics": "text/calendar",
|
||||
".icz": "text/calendar",
|
||||
".idc": "text/plain",
|
||||
".ief": "image/ief",
|
||||
".iefs": "image/ief",
|
||||
".iges": "application/iges",
|
||||
".igs": "application/iges",
|
||||
".ima": "application/x-ima",
|
||||
".imap": "application/x-httpd-imap",
|
||||
".inf": "application/inf",
|
||||
".ins": "application/x-internett-signup",
|
||||
".ip": "application/x-ip2",
|
||||
".isu": "video/x-isvideo",
|
||||
".it": "audio/it",
|
||||
".iv": "application/x-inventor",
|
||||
".ivr": "i-world/i-vrml",
|
||||
".ivy": "application/x-livescreen",
|
||||
".jam": "audio/x-jam",
|
||||
".jav": "text/x-java-source",
|
||||
".java": "text/x-java-source",
|
||||
".jcm": "application/x-java-commerce",
|
||||
".jfif-tbnl": "image/jpeg",
|
||||
".jfif": "image/jpeg",
|
||||
".jnlp": "application/x-java-jnlp-file",
|
||||
".jpe": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".jpg": "image/jpeg",
|
||||
".jps": "image/x-jps",
|
||||
".js": "application/javascript",
|
||||
".json": "application/json",
|
||||
".jut": "image/jutvision",
|
||||
".kar": "audio/midi",
|
||||
".karbon": "application/vnd.kde.karbon",
|
||||
".kfo": "application/vnd.kde.kformula",
|
||||
".flw": "application/vnd.kde.kivio",
|
||||
".kml": "application/vnd.google-earth.kml+xml",
|
||||
".kmz": "application/vnd.google-earth.kmz",
|
||||
".kon": "application/vnd.kde.kontour",
|
||||
".kpr": "application/vnd.kde.kpresenter",
|
||||
".kpt": "application/vnd.kde.kpresenter",
|
||||
".ksp": "application/vnd.kde.kspread",
|
||||
".kwd": "application/vnd.kde.kword",
|
||||
".kwt": "application/vnd.kde.kword",
|
||||
".ksh": "text/x-scriptksh",
|
||||
".la": "audio/nspaudio",
|
||||
".lam": "audio/x-liveaudio",
|
||||
".latex": "application/x-latex",
|
||||
".lha": "application/lha",
|
||||
".lhx": "application/octet-stream",
|
||||
".list": "text/plain",
|
||||
".lma": "audio/nspaudio",
|
||||
".log": "text/plain",
|
||||
".lsp": "text/x-scriptlisp",
|
||||
".lst": "text/plain",
|
||||
".lsx": "text/x-la-asf",
|
||||
".ltx": "application/x-latex",
|
||||
".lzh": "application/octet-stream",
|
||||
".lzx": "application/lzx",
|
||||
".m1v": "video/mpeg",
|
||||
".m2a": "audio/mpeg",
|
||||
".m2v": "video/mpeg",
|
||||
".m3u": "audio/x-mpegurl",
|
||||
".m": "text/x-m",
|
||||
".man": "application/x-troff-man",
|
||||
".manifest": "text/cache-manifest",
|
||||
".map": "application/x-navimap",
|
||||
".mar": "text/plain",
|
||||
".mbd": "application/mbedlet",
|
||||
".mc$": "application/x-magic-cap-package-10",
|
||||
".mcd": "application/mcad",
|
||||
".mcf": "text/mcf",
|
||||
".mcp": "application/netmc",
|
||||
".me": "application/x-troff-me",
|
||||
".mht": "message/rfc822",
|
||||
".mhtml": "message/rfc822",
|
||||
".mid": "application/x-midi",
|
||||
".midi": "application/x-midi",
|
||||
".mif": "application/x-frame",
|
||||
".mime": "message/rfc822",
|
||||
".mjf": "audio/x-vndaudioexplosionmjuicemediafile",
|
||||
".mjpg": "video/x-motion-jpeg",
|
||||
".mm": "application/base64",
|
||||
".mme": "application/base64",
|
||||
".mod": "audio/mod",
|
||||
".moov": "video/quicktime",
|
||||
".mov": "video/quicktime",
|
||||
".movie": "video/x-sgi-movie",
|
||||
".mp2": "audio/mpeg",
|
||||
".mp3": "audio/mpeg3",
|
||||
".mp4": "video/mp4",
|
||||
".mpa": "audio/mpeg",
|
||||
".mpc": "application/x-project",
|
||||
".mpe": "video/mpeg",
|
||||
".mpeg": "video/mpeg",
|
||||
".mpg": "video/mpeg",
|
||||
".mpga": "audio/mpeg",
|
||||
".mpp": "application/vndms-project",
|
||||
".mpt": "application/x-project",
|
||||
".mpv": "application/x-project",
|
||||
".mpx": "application/x-project",
|
||||
".mrc": "application/marc",
|
||||
".ms": "application/x-troff-ms",
|
||||
".mv": "video/x-sgi-movie",
|
||||
".my": "audio/make",
|
||||
".mzz": "application/x-vndaudioexplosionmzz",
|
||||
".nap": "image/naplps",
|
||||
".naplps": "image/naplps",
|
||||
".nc": "application/x-netcdf",
|
||||
".ncm": "application/vndnokiaconfiguration-message",
|
||||
".nif": "image/x-niff",
|
||||
".niff": "image/x-niff",
|
||||
".nix": "application/x-mix-transfer",
|
||||
".nsc": "application/x-conference",
|
||||
".nvd": "application/x-navidoc",
|
||||
".o": "application/octet-stream",
|
||||
".oda": "application/oda",
|
||||
".odb": "application/vnd.oasis.opendocument.database",
|
||||
".odc": "application/vnd.oasis.opendocument.chart",
|
||||
".odf": "application/vnd.oasis.opendocument.formula",
|
||||
".odg": "application/vnd.oasis.opendocument.graphics",
|
||||
".odi": "application/vnd.oasis.opendocument.image",
|
||||
".odm": "application/vnd.oasis.opendocument.text-master",
|
||||
".odp": "application/vnd.oasis.opendocument.presentation",
|
||||
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
||||
".odt": "application/vnd.oasis.opendocument.text",
|
||||
".oga": "audio/ogg",
|
||||
".ogg": "audio/ogg",
|
||||
".ogv": "video/ogg",
|
||||
".omc": "application/x-omc",
|
||||
".omcd": "application/x-omcdatamaker",
|
||||
".omcr": "application/x-omcregerator",
|
||||
".otc": "application/vnd.oasis.opendocument.chart-template",
|
||||
".otf": "application/vnd.oasis.opendocument.formula-template",
|
||||
".otg": "application/vnd.oasis.opendocument.graphics-template",
|
||||
".oth": "application/vnd.oasis.opendocument.text-web",
|
||||
".oti": "application/vnd.oasis.opendocument.image-template",
|
||||
".otm": "application/vnd.oasis.opendocument.text-master",
|
||||
".otp": "application/vnd.oasis.opendocument.presentation-template",
|
||||
".ots": "application/vnd.oasis.opendocument.spreadsheet-template",
|
||||
".ott": "application/vnd.oasis.opendocument.text-template",
|
||||
".p10": "application/pkcs10",
|
||||
".p12": "application/pkcs-12",
|
||||
".p7a": "application/x-pkcs7-signature",
|
||||
".p7c": "application/pkcs7-mime",
|
||||
".p7m": "application/pkcs7-mime",
|
||||
".p7r": "application/x-pkcs7-certreqresp",
|
||||
".p7s": "application/pkcs7-signature",
|
||||
".p": "text/x-pascal",
|
||||
".part": "application/pro_eng",
|
||||
".pas": "text/pascal",
|
||||
".pbm": "image/x-portable-bitmap",
|
||||
".pcl": "application/vndhp-pcl",
|
||||
".pct": "image/x-pict",
|
||||
".pcx": "image/x-pcx",
|
||||
".pdb": "chemical/x-pdb",
|
||||
".pdf": "application/pdf",
|
||||
".pfunk": "audio/make",
|
||||
".pgm": "image/x-portable-graymap",
|
||||
".pic": "image/pict",
|
||||
".pict": "image/pict",
|
||||
".pkg": "application/x-newton-compatible-pkg",
|
||||
".pko": "application/vndms-pkipko",
|
||||
".pl": "text/x-scriptperl",
|
||||
".plx": "application/x-pixclscript",
|
||||
".pm4": "application/x-pagemaker",
|
||||
".pm5": "application/x-pagemaker",
|
||||
".pm": "text/x-scriptperl-module",
|
||||
".png": "image/png",
|
||||
".pnm": "application/x-portable-anymap",
|
||||
".pot": "application/mspowerpoint",
|
||||
".pov": "model/x-pov",
|
||||
".ppa": "application/vndms-powerpoint",
|
||||
".ppm": "image/x-portable-pixmap",
|
||||
".pps": "application/mspowerpoint",
|
||||
".ppt": "application/mspowerpoint",
|
||||
".ppz": "application/mspowerpoint",
|
||||
".pre": "application/x-freelance",
|
||||
".prt": "application/pro_eng",
|
||||
".ps": "application/postscript",
|
||||
".psd": "application/octet-stream",
|
||||
".pvu": "paleovu/x-pv",
|
||||
".pwz": "application/vndms-powerpoint",
|
||||
".py": "text/x-scriptphyton",
|
||||
".pyc": "application/x-bytecodepython",
|
||||
".qcp": "audio/vndqcelp",
|
||||
".qd3": "x-world/x-3dmf",
|
||||
".qd3d": "x-world/x-3dmf",
|
||||
".qif": "image/x-quicktime",
|
||||
".qt": "video/quicktime",
|
||||
".qtc": "video/x-qtc",
|
||||
".qti": "image/x-quicktime",
|
||||
".qtif": "image/x-quicktime",
|
||||
".ra": "audio/x-pn-realaudio",
|
||||
".ram": "audio/x-pn-realaudio",
|
||||
".rar": "application/x-rar-compressed",
|
||||
".ras": "application/x-cmu-raster",
|
||||
".rast": "image/cmu-raster",
|
||||
".rexx": "text/x-scriptrexx",
|
||||
".rf": "image/vndrn-realflash",
|
||||
".rgb": "image/x-rgb",
|
||||
".rm": "application/vndrn-realmedia",
|
||||
".rmi": "audio/mid",
|
||||
".rmm": "audio/x-pn-realaudio",
|
||||
".rmp": "audio/x-pn-realaudio",
|
||||
".rng": "application/ringing-tones",
|
||||
".rnx": "application/vndrn-realplayer",
|
||||
".roff": "application/x-troff",
|
||||
".rp": "image/vndrn-realpix",
|
||||
".rpm": "audio/x-pn-realaudio-plugin",
|
||||
".rt": "text/vndrn-realtext",
|
||||
".rtf": "text/richtext",
|
||||
".rtx": "text/richtext",
|
||||
".rv": "video/vndrn-realvideo",
|
||||
".s": "text/x-asm",
|
||||
".s3m": "audio/s3m",
|
||||
".s7z": "application/x-7z-compressed",
|
||||
".saveme": "application/octet-stream",
|
||||
".sbk": "application/x-tbook",
|
||||
".scm": "text/x-scriptscheme",
|
||||
".sdml": "text/plain",
|
||||
".sdp": "application/sdp",
|
||||
".sdr": "application/sounder",
|
||||
".sea": "application/sea",
|
||||
".set": "application/set",
|
||||
".sgm": "text/x-sgml",
|
||||
".sgml": "text/x-sgml",
|
||||
".sh": "text/x-scriptsh",
|
||||
".shar": "application/x-bsh",
|
||||
".shtml": "text/x-server-parsed-html",
|
||||
".sid": "audio/x-psid",
|
||||
".skd": "application/x-koan",
|
||||
".skm": "application/x-koan",
|
||||
".skp": "application/x-koan",
|
||||
".skt": "application/x-koan",
|
||||
".sit": "application/x-stuffit",
|
||||
".sitx": "application/x-stuffitx",
|
||||
".sl": "application/x-seelogo",
|
||||
".smi": "application/smil",
|
||||
".smil": "application/smil",
|
||||
".snd": "audio/basic",
|
||||
".sol": "application/solids",
|
||||
".spc": "text/x-speech",
|
||||
".spl": "application/futuresplash",
|
||||
".spr": "application/x-sprite",
|
||||
".sprite": "application/x-sprite",
|
||||
".spx": "audio/ogg",
|
||||
".src": "application/x-wais-source",
|
||||
".ssi": "text/x-server-parsed-html",
|
||||
".ssm": "application/streamingmedia",
|
||||
".sst": "application/vndms-pkicertstore",
|
||||
".step": "application/step",
|
||||
".stl": "application/sla",
|
||||
".stp": "application/step",
|
||||
".sv4cpio": "application/x-sv4cpio",
|
||||
".sv4crc": "application/x-sv4crc",
|
||||
".svf": "image/vnddwg",
|
||||
".svg": "image/svg+xml",
|
||||
".svr": "application/x-world",
|
||||
".swf": "application/x-shockwave-flash",
|
||||
".t": "application/x-troff",
|
||||
".talk": "text/x-speech",
|
||||
".tar": "application/x-tar",
|
||||
".tbk": "application/toolbook",
|
||||
".tcl": "text/x-scripttcl",
|
||||
".tcsh": "text/x-scripttcsh",
|
||||
".tex": "application/x-tex",
|
||||
".texi": "application/x-texinfo",
|
||||
".texinfo": "application/x-texinfo",
|
||||
".text": "text/plain",
|
||||
".tgz": "application/gnutar",
|
||||
".tif": "image/tiff",
|
||||
".tiff": "image/tiff",
|
||||
".tr": "application/x-troff",
|
||||
".tsi": "audio/tsp-audio",
|
||||
".tsp": "application/dsptype",
|
||||
".tsv": "text/tab-separated-values",
|
||||
".turbot": "image/florian",
|
||||
".txt": "text/plain",
|
||||
".uil": "text/x-uil",
|
||||
".uni": "text/uri-list",
|
||||
".unis": "text/uri-list",
|
||||
".unv": "application/i-deas",
|
||||
".uri": "text/uri-list",
|
||||
".uris": "text/uri-list",
|
||||
".ustar": "application/x-ustar",
|
||||
".uu": "text/x-uuencode",
|
||||
".uue": "text/x-uuencode",
|
||||
".vcd": "application/x-cdlink",
|
||||
".vcf": "text/x-vcard",
|
||||
".vcard": "text/x-vcard",
|
||||
".vcs": "text/x-vcalendar",
|
||||
".vda": "application/vda",
|
||||
".vdo": "video/vdo",
|
||||
".vew": "application/groupwise",
|
||||
".viv": "video/vivo",
|
||||
".vivo": "video/vivo",
|
||||
".vmd": "application/vocaltec-media-desc",
|
||||
".vmf": "application/vocaltec-media-file",
|
||||
".voc": "audio/voc",
|
||||
".vos": "video/vosaic",
|
||||
".vox": "audio/voxware",
|
||||
".vqe": "audio/x-twinvq-plugin",
|
||||
".vqf": "audio/x-twinvq",
|
||||
".vql": "audio/x-twinvq-plugin",
|
||||
".vrml": "application/x-vrml",
|
||||
".vrt": "x-world/x-vrt",
|
||||
".vsd": "application/x-visio",
|
||||
".vst": "application/x-visio",
|
||||
".vsw": "application/x-visio",
|
||||
".w60": "application/wordperfect60",
|
||||
".w61": "application/wordperfect61",
|
||||
".w6w": "application/msword",
|
||||
".wav": "audio/wav",
|
||||
".wb1": "application/x-qpro",
|
||||
".wbmp": "image/vnd.wap.wbmp",
|
||||
".web": "application/vndxara",
|
||||
".wiz": "application/msword",
|
||||
".wk1": "application/x-123",
|
||||
".wmf": "windows/metafile",
|
||||
".wml": "text/vnd.wap.wml",
|
||||
".wmlc": "application/vnd.wap.wmlc",
|
||||
".wmls": "text/vnd.wap.wmlscript",
|
||||
".wmlsc": "application/vnd.wap.wmlscriptc",
|
||||
".word": "application/msword",
|
||||
".wp5": "application/wordperfect",
|
||||
".wp6": "application/wordperfect",
|
||||
".wp": "application/wordperfect",
|
||||
".wpd": "application/wordperfect",
|
||||
".wq1": "application/x-lotus",
|
||||
".wri": "application/mswrite",
|
||||
".wrl": "application/x-world",
|
||||
".wrz": "model/vrml",
|
||||
".wsc": "text/scriplet",
|
||||
".wsrc": "application/x-wais-source",
|
||||
".wtk": "application/x-wintalk",
|
||||
".x-png": "image/png",
|
||||
".xbm": "image/x-xbitmap",
|
||||
".xdr": "video/x-amt-demorun",
|
||||
".xgz": "xgl/drawing",
|
||||
".xif": "image/vndxiff",
|
||||
".xl": "application/excel",
|
||||
".xla": "application/excel",
|
||||
".xlb": "application/excel",
|
||||
".xlc": "application/excel",
|
||||
".xld": "application/excel",
|
||||
".xlk": "application/excel",
|
||||
".xll": "application/excel",
|
||||
".xlm": "application/excel",
|
||||
".xls": "application/excel",
|
||||
".xlt": "application/excel",
|
||||
".xlv": "application/excel",
|
||||
".xlw": "application/excel",
|
||||
".xm": "audio/xm",
|
||||
".xml": "text/xml",
|
||||
".xmz": "xgl/movie",
|
||||
".xpix": "application/x-vndls-xpix",
|
||||
".xpm": "image/x-xpixmap",
|
||||
".xsr": "video/x-amt-showrun",
|
||||
".xwd": "image/x-xwd",
|
||||
".xyz": "chemical/x-pdb",
|
||||
".z": "application/x-compress",
|
||||
".zip": "application/zip",
|
||||
".zoo": "application/octet-stream",
|
||||
".zsh": "text/x-scriptzsh",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".docm": "application/vnd.ms-word.document.macroEnabled.12",
|
||||
".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
||||
".dotm": "application/vnd.ms-word.template.macroEnabled.12",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
|
||||
".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||
".xltm": "application/vnd.ms-excel.template.macroEnabled.12",
|
||||
".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
||||
".xlam": "application/vnd.ms-excel.addin.macroEnabled.12",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
||||
".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
||||
".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
||||
".potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
|
||||
".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
|
||||
".ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12",
|
||||
".sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide",
|
||||
".sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12",
|
||||
".thmx": "application/vnd.ms-officetheme",
|
||||
".onetoc": "application/onenote",
|
||||
".onetoc2": "application/onenote",
|
||||
".onetmp": "application/onenote",
|
||||
".onepkg": "application/onenote",
|
||||
".xpi": "application/x-xpinstall",
|
||||
}
|
||||
|
||||
func init() {
|
||||
for ext, typ := range types {
|
||||
// skip errors
|
||||
mime.AddExtensionType(ext, typ)
|
||||
}
|
||||
}
|
||||
|
||||
// TypeByExtension returns the MIME type associated with the file extension ext.
|
||||
// The extension ext should begin with a leading dot, as in ".html".
|
||||
// When ext has no associated type, typeByExtension returns "".
|
||||
//
|
||||
// Extensions are looked up first case-sensitively, then case-insensitively.
|
||||
//
|
||||
// The built-in table is small but on unix it is augmented by the local
|
||||
// system's mime.types file(s) if available under one or more of these
|
||||
// names:
|
||||
//
|
||||
// /etc/mime.types
|
||||
// /etc/apache2/mime.types
|
||||
// /etc/apache/mime.types
|
||||
//
|
||||
// On Windows, MIME types are extracted from the registry.
|
||||
//
|
||||
// Text types have the charset parameter set to "utf-8" by default.
|
||||
func TypeByExtension(ext string) (typ string) {
|
||||
if len(ext) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
if ext[0] != '.' { // try to take it by filename
|
||||
typ = TypeByFilename(ext)
|
||||
if typ == "" {
|
||||
ext = "." + ext // if error or something wrong then prepend the dot
|
||||
}
|
||||
}
|
||||
|
||||
if typ == "" {
|
||||
typ = mime.TypeByExtension(ext)
|
||||
}
|
||||
|
||||
// mime.TypeByExtension returns as text/plain; | charset=utf-8 the static .js (not always)
|
||||
if ext == ".js" && (typ == "text/plain" || typ == "text/plain; charset=utf-8") {
|
||||
|
||||
if ext == ".js" {
|
||||
typ = "application/javascript"
|
||||
}
|
||||
}
|
||||
return typ
|
||||
}
|
||||
|
||||
// TypeByFilename, saem as TypeByExtension
|
||||
// but receives a filename path instead.
|
||||
func TypeByFilename(fullFilename string) string {
|
||||
ext := filepath.Ext(fullFilename)
|
||||
return TypeByExtension(ext)
|
||||
}
|
||||
171
core/router/party.go
Normal file
171
core/router/party.go
Normal file
@@ -0,0 +1,171 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/context"
|
||||
) // Party is here to separate the concept of
|
||||
// api builder and the sub api builder.
|
||||
|
||||
// Party is just a group joiner of routes which have the same prefix and share same middleware(s) also.
|
||||
// Party could also be named as 'Join' or 'Node' or 'Group' , Party chosen because it is fun.
|
||||
//
|
||||
// Look the "APIBuilder" for its implementation.
|
||||
type Party interface {
|
||||
// Party creates and returns a new child Party with the following features.
|
||||
Party(relativePath string, handlers ...context.Handler) Party
|
||||
|
||||
// Use appends Handler(s) to the current Party's routes and child routes.
|
||||
// If the current Party is the root, then it registers the middleware to all child Parties' routes too.
|
||||
Use(handlers ...context.Handler)
|
||||
|
||||
// Done appends to the very end, Handler(s) to the current Party's routes and child routes
|
||||
// The difference from .Use is that this/or these Handler(s) are being always running last.
|
||||
Done(handlers ...context.Handler)
|
||||
|
||||
// Handle registers a route to the server's router.
|
||||
// if empty method is passed then handler(s) are being registered to all methods, same as .Any.
|
||||
//
|
||||
// Returns the read-only route information.
|
||||
Handle(method string, registeredPath string, handlers ...context.Handler) (*Route, error)
|
||||
|
||||
// None registers an "offline" route
|
||||
// see context.ExecRoute(routeName) and
|
||||
// party.Routes().Online(handleResultregistry.*Route, "GET") and
|
||||
// Offline(handleResultregistry.*Route)
|
||||
//
|
||||
// Returns the read-only route information.
|
||||
None(path string, handlers ...context.Handler) (*Route, error)
|
||||
|
||||
// Get registers a route for the Get http method.
|
||||
//
|
||||
// Returns the read-only route information.
|
||||
Get(path string, handlers ...context.Handler) (*Route, error)
|
||||
// Post registers a route for the Post http method.
|
||||
//
|
||||
// Returns the read-only route information.
|
||||
Post(path string, handlers ...context.Handler) (*Route, error)
|
||||
// Put registers a route for the Put http method.
|
||||
//
|
||||
// Returns the read-only route information.
|
||||
Put(path string, handlers ...context.Handler) (*Route, error)
|
||||
// Delete registers a route for the Delete http method.
|
||||
//
|
||||
// Returns the read-only route information.
|
||||
Delete(path string, handlers ...context.Handler) (*Route, error)
|
||||
// Connect registers a route for the Connect http method.
|
||||
//
|
||||
// Returns the read-only route information.
|
||||
Connect(path string, handlers ...context.Handler) (*Route, error)
|
||||
// Head registers a route for the Head http method.
|
||||
//
|
||||
// Returns the read-only route information.
|
||||
Head(path string, handlers ...context.Handler) (*Route, error)
|
||||
// Options registers a route for the Options http method.
|
||||
//
|
||||
// Returns the read-only route information.
|
||||
Options(path string, handlers ...context.Handler) (*Route, error)
|
||||
// Patch registers a route for the Patch http method.
|
||||
//
|
||||
// Returns the read-only route information.
|
||||
Patch(path string, handlers ...context.Handler) (*Route, error)
|
||||
// Trace registers a route for the Trace http method.
|
||||
//
|
||||
// Returns the read-only route information.
|
||||
Trace(path string, handlers ...context.Handler) (*Route, error)
|
||||
// Any registers a route for ALL of the http methods
|
||||
// (Get,Post,Put,Head,Patch,Options,Connect,Delete).
|
||||
Any(registeredPath string, handlers ...context.Handler) error
|
||||
|
||||
// StaticHandler returns a new Handler which is ready
|
||||
// to serve all kind of static files.
|
||||
//
|
||||
// Note:
|
||||
// The only difference from package-level `StaticHandler`
|
||||
// is that this `StaticHandler`` receives a request path which
|
||||
// is appended to the party's relative path and stripped here,
|
||||
// so `iris.StripPath` is useless and should not being used here.
|
||||
//
|
||||
// Usage:
|
||||
// app := iris.New()
|
||||
// ...
|
||||
// mySubdomainFsServer := app.Party("mysubdomain.")
|
||||
// h := mySubdomainFsServer.StaticHandler("/static", "./static_files", false, false)
|
||||
// /* http://mysubdomain.mydomain.com/static/css/style.css */
|
||||
// mySubdomainFsServer.Get("/static", h)
|
||||
// ...
|
||||
//
|
||||
StaticHandler(requestPath string, systemPath string, showList bool, enableGzip bool, exceptRoutes ...*Route) context.Handler
|
||||
|
||||
// StaticServe serves a directory as web resource
|
||||
// it's the simpliest form of the Static* functions
|
||||
// Almost same usage as StaticWeb
|
||||
// accepts only one required parameter which is the systemPath,
|
||||
// the same path will be used to register the GET and HEAD method routes.
|
||||
// If second parameter is empty, otherwise the requestPath is the second parameter
|
||||
// it uses gzip compression (compression on each request, no file cache).
|
||||
//
|
||||
// Returns the GET *Route.
|
||||
StaticServe(systemPath string, requestPath ...string) (*Route, error)
|
||||
// StaticContent registers a GET and HEAD method routes to the requestPath
|
||||
// that are ready to serve raw static bytes, memory cached.
|
||||
//
|
||||
// Returns the GET *Route.
|
||||
StaticContent(requestPath string, cType string, content []byte) (*Route, error)
|
||||
// StaticEmbedded used when files are distributed inside the app executable, using go-bindata mostly
|
||||
// First parameter is the request path, the path which the files in the vdir will be served to, for example "/static"
|
||||
// Second parameter is the (virtual) directory path, for example "./assets"
|
||||
// Third parameter is the Asset function
|
||||
// Forth parameter is the AssetNames function.
|
||||
//
|
||||
// Returns the GET *Route.
|
||||
//
|
||||
// Example: https://github.com/kataras/iris/tree/master/_examples/intermediate/serve-embedded-files
|
||||
StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) (*Route, error)
|
||||
|
||||
// Favicon serves static favicon
|
||||
// accepts 2 parameters, second is optional
|
||||
// favPath (string), declare the system directory path of the __.ico
|
||||
// requestPath (string), it's the route's path, by default this is the "/favicon.ico" because some browsers tries to get this by default first,
|
||||
// you can declare your own path if you have more than one favicon (desktop, mobile and so on)
|
||||
//
|
||||
// this func will add a route for you which will static serve the /yuorpath/yourfile.ico to the /yourfile.ico
|
||||
// (nothing special that you can't handle by yourself).
|
||||
// Note that you have to call it on every favicon you have to serve automatically (desktop, mobile and so on).
|
||||
//
|
||||
// Returns the GET *Route.
|
||||
Favicon(favPath string, requestPath ...string) (*Route, error)
|
||||
// StaticWeb returns a handler that serves HTTP requests
|
||||
// with the contents of the file system rooted at directory.
|
||||
//
|
||||
// first parameter: the route path
|
||||
// second parameter: the system directory
|
||||
// third OPTIONAL parameter: the exception routes
|
||||
// (= give priority to these routes instead of the static handler)
|
||||
// for more options look router.StaticHandler.
|
||||
//
|
||||
// router.StaticWeb("/static", "./static")
|
||||
//
|
||||
// As a special case, the returned file server redirects any request
|
||||
// ending in "/index.html" to the same path, without the final
|
||||
// "index.html".
|
||||
//
|
||||
// StaticWeb calls the StaticHandler(requestPath, systemPath, listingDirectories: false, gzip: false ).
|
||||
//
|
||||
// Returns the GET *Route.
|
||||
StaticWeb(requestPath string, systemPath string, exceptRoutes ...*Route) (*Route, error)
|
||||
|
||||
// Layout oerrides the parent template layout with a more specific layout for this Party
|
||||
// returns this Party, to continue as normal
|
||||
// Usage:
|
||||
// app := iris.New()
|
||||
// my := app.Party("/my").Layout("layouts/mylayout.html")
|
||||
// {
|
||||
// my.Get("/", func(ctx context.Context) {
|
||||
// ctx.MustRender("page1.html", nil)
|
||||
// })
|
||||
// }
|
||||
Layout(tmplLayoutFile string) Party
|
||||
}
|
||||
252
core/router/path.go
Normal file
252
core/router/path.go
Normal file
@@ -0,0 +1,252 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/esemplastic/unis"
|
||||
"github.com/kataras/iris/core/nettools"
|
||||
)
|
||||
|
||||
const (
|
||||
// ParamStart the character in string representation where the underline router starts its dynamic named parameter.
|
||||
ParamStart = ":"
|
||||
// WildcardParamStart the character in string representation where the underline router starts its dynamic wildcard
|
||||
// path parameter.
|
||||
WildcardParamStart = "*"
|
||||
)
|
||||
|
||||
// ResolveStaticPath receives a (dynamic) path and tries to return its static path part.
|
||||
func ResolveStaticPath(original string) string {
|
||||
i := strings.Index(original, ParamStart)
|
||||
v := strings.Index(original, WildcardParamStart)
|
||||
|
||||
return unis.NewChain(
|
||||
unis.NewConditional(
|
||||
unis.NewRangeEnd(i),
|
||||
unis.NewRangeEnd(v)),
|
||||
cleanPath).Process(original)
|
||||
}
|
||||
|
||||
// Param receives a parameter name prefixed with the ParamStart symbol.
|
||||
func Param(name string) string {
|
||||
return prefix(name, ParamStart)
|
||||
}
|
||||
|
||||
// WildcardParam receives a parameter name prefixed with the WildcardParamStart symbol.
|
||||
func WildcardParam(name string) string {
|
||||
if len(name) == 0 {
|
||||
return ""
|
||||
}
|
||||
return prefix(name, WildcardParamStart)
|
||||
}
|
||||
|
||||
func prefix(str string, prefix string) string {
|
||||
return unis.NewExclusivePrepender(prefix).Process(str)
|
||||
}
|
||||
|
||||
func joinPath(path1 string, path2 string) string {
|
||||
return path.Join(path1, path2)
|
||||
}
|
||||
|
||||
// cleanPath applies the following rules
|
||||
// iteratively until no further processing can be done:
|
||||
//
|
||||
// 1. Replace multiple slashes with a single slash.
|
||||
// 3. Eliminate each inner .. path name element (the parent directory)
|
||||
// along with the non-.. element that precedes it.
|
||||
// 4. Eliminate .. elements that begin a rooted path:
|
||||
// that is, replace "/.." by "/" at the beginning of a path.
|
||||
//
|
||||
// The returned path ends in a slash only if it is the root "/".
|
||||
var cleanPath = unis.NewChain(
|
||||
|
||||
unis.NewSuffixRemover("/"),
|
||||
unis.NewTargetedJoiner(0, '/'),
|
||||
unis.ProcessorFunc(path.Clean),
|
||||
unis.NewReplacer(map[string]string{
|
||||
"//": "/",
|
||||
"\\": "/",
|
||||
}),
|
||||
unis.ProcessorFunc(func(s string) string {
|
||||
if s == "" || s == "." {
|
||||
return "/"
|
||||
}
|
||||
return s
|
||||
}),
|
||||
)
|
||||
|
||||
const (
|
||||
// DynamicSubdomainIndicator where a registered path starts with '*.' then it contains a dynamic subdomain, if subdomain == "*." then its dynamic
|
||||
//
|
||||
// used internally by URLPath and the router serve.
|
||||
DynamicSubdomainIndicator = "*."
|
||||
// SubdomainIndicator where './' exists in a registered path then it contains subdomain
|
||||
//
|
||||
// used on router builder
|
||||
SubdomainIndicator = "./"
|
||||
)
|
||||
|
||||
func newSubdomainDivider(sep string) unis.DividerFunc {
|
||||
// invert if indiciator not found
|
||||
// because we need the first parameter to be the subdomain
|
||||
// even if empty, but the second parameter
|
||||
// should be the path, in order to normalize it
|
||||
// (because of the reason of subdomains shouldn't be normalized as path)
|
||||
subdomainDevider := unis.NewInvertOnFailureDivider(unis.NewDivider(sep))
|
||||
return func(fullpath string) (string, string) {
|
||||
subdomain, path := subdomainDevider.Divide(fullpath)
|
||||
return subdomain, path //cleanPath(path)
|
||||
}
|
||||
}
|
||||
|
||||
// ExctractSubdomain checks if the path has subdomain and if it's
|
||||
// it splits the subdomain and path and returns them, otherwise it returns
|
||||
// an empty subdomain and the given path.
|
||||
//
|
||||
// First return value is the subdomain, second is the path.
|
||||
var exctractSubdomain = newSubdomainDivider(SubdomainIndicator)
|
||||
|
||||
// RoutePathReverserOption option signature for the RoutePathReverser.
|
||||
type RoutePathReverserOption func(*RoutePathReverser)
|
||||
|
||||
// WithScheme is an option for the RoutepathReverser,
|
||||
// it sets the optional field "vscheme",
|
||||
// v for virtual.
|
||||
// if vscheme is empty then it will try to resolve it from
|
||||
// the RoutePathReverser's vhost field.
|
||||
//
|
||||
// See WithHost or WithServer to enable the URL feature.
|
||||
func WithScheme(scheme string) RoutePathReverserOption {
|
||||
return func(ps *RoutePathReverser) {
|
||||
ps.vscheme = scheme
|
||||
}
|
||||
}
|
||||
|
||||
// WithHost enables the RoutePathReverser's URL feature.
|
||||
// Both "WithHost" and "WithScheme" can be different from
|
||||
// the real server's listening address, i.e nginx in front.
|
||||
func WithHost(host string) RoutePathReverserOption {
|
||||
return func(ps *RoutePathReverser) {
|
||||
ps.vhost = host
|
||||
if ps.vscheme == "" {
|
||||
ps.vscheme = nettools.ResolveScheme(host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithServer enables the RoutePathReverser's URL feature.
|
||||
// It receives an *http.Server and tries to resolve
|
||||
// a scheme and a host to be used in the URL function.
|
||||
func WithServer(srv *http.Server) RoutePathReverserOption {
|
||||
return func(ps *RoutePathReverser) {
|
||||
ps.vhost = nettools.ResolveVHost(srv.Addr)
|
||||
ps.vscheme = nettools.ResolveSchemeFromServer(srv)
|
||||
}
|
||||
}
|
||||
|
||||
// RoutePathReverser contains methods that helps to reverse a
|
||||
// (dynamic) path from a specific route,
|
||||
// route name is required because a route may being registered
|
||||
// on more than one http method.
|
||||
type RoutePathReverser struct {
|
||||
provider RoutesProvider
|
||||
// both vhost and vscheme are being used, optionally, for the URL feature.
|
||||
vhost string
|
||||
vscheme string
|
||||
}
|
||||
|
||||
// NewRoutePathReverser returns a new path reverser based on
|
||||
// a routes provider, needed to get a route based on its name.
|
||||
// Options is required for the URL function.
|
||||
// See WithScheme and WithHost or WithServer.
|
||||
func NewRoutePathReverser(apiRoutesProvider RoutesProvider, options ...RoutePathReverserOption) *RoutePathReverser {
|
||||
ps := &RoutePathReverser{
|
||||
provider: apiRoutesProvider,
|
||||
}
|
||||
for _, o := range options {
|
||||
o(ps)
|
||||
}
|
||||
return ps
|
||||
}
|
||||
|
||||
// Path returns a route path based on a route name and any dynamic named parameter's values-only.
|
||||
func (ps *RoutePathReverser) Path(routeName string, paramValues ...interface{}) string {
|
||||
r := ps.provider.GetRoute(routeName)
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(paramValues) == 0 {
|
||||
return r.Path
|
||||
}
|
||||
|
||||
return r.ResolvePath(toStringSlice(paramValues)...)
|
||||
}
|
||||
|
||||
func toStringSlice(args []interface{}) []string {
|
||||
var argsString []string
|
||||
if len(args) > 0 {
|
||||
argsString = make([]string, len(args), len(args))
|
||||
}
|
||||
|
||||
for i, v := range args {
|
||||
if s, ok := v.(string); ok {
|
||||
argsString[i] = s
|
||||
} else if num, ok := v.(int); ok {
|
||||
argsString[i] = strconv.Itoa(num)
|
||||
} else if b, ok := v.(bool); ok {
|
||||
argsString[i] = strconv.FormatBool(b)
|
||||
} else if arr, ok := v.([]string); ok {
|
||||
if len(arr) > 0 {
|
||||
argsString[i] = arr[0]
|
||||
argsString = append(argsString, arr[1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return argsString
|
||||
}
|
||||
|
||||
// Remove the URL for now, it complicates things for the whole framework without a specific benefits,
|
||||
// developers can just concat the subdomain, (host can be auto-retrieve by browser using the Path).
|
||||
|
||||
// URL same as Path but returns the full uri, i.e https://mysubdomain.mydomain.com/hello/kataras
|
||||
func (ps *RoutePathReverser) URL(routeName string, paramValues ...interface{}) (url string) {
|
||||
if ps.vhost == "" || ps.vscheme == "" {
|
||||
return "not supported"
|
||||
}
|
||||
|
||||
r := ps.provider.GetRoute(routeName)
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(paramValues) == 0 {
|
||||
return r.Path
|
||||
}
|
||||
|
||||
args := toStringSlice(paramValues)
|
||||
|
||||
host := ps.vhost
|
||||
scheme := ps.vscheme
|
||||
// if it's dynamic subdomain then the first argument is the subdomain part
|
||||
// for this part we are responsible not the custom routers
|
||||
if r.Subdomain == DynamicSubdomainIndicator {
|
||||
subdomain := args[0]
|
||||
host = subdomain + "." + host
|
||||
args = args[1:] // remove the subdomain part for the arguments,
|
||||
|
||||
}
|
||||
|
||||
if parsedPath := r.ResolvePath(args...); parsedPath != "" {
|
||||
url = scheme + "://" + host + parsedPath
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
128
core/router/route.go
Normal file
128
core/router/route.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/router/macro"
|
||||
)
|
||||
|
||||
type Route struct {
|
||||
Name string // "userRoute"
|
||||
Method string // "GET"
|
||||
Subdomain string // "admin."
|
||||
tmpl *macro.Template // Tmpl().Src: "/api/user/{id:int}"
|
||||
Path string // "/api/user/:id"
|
||||
Handlers context.Handlers
|
||||
// FormattedPath all dynamic named parameters (if any) replaced with %v,
|
||||
// used by Application to validate param values of a Route based on its name.
|
||||
FormattedPath string
|
||||
}
|
||||
|
||||
func NewRoute(method, subdomain, unparsedPath string,
|
||||
handlers context.Handlers, macros *macro.MacroMap) (*Route, error) {
|
||||
|
||||
tmpl, err := macro.Parse(unparsedPath, macros)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path, handlers, err := compileRoutePathAndHandlers(handlers, tmpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path = cleanPath(path) // maybe unnecessary here but who cares in this moment
|
||||
defaultName := method + subdomain + path
|
||||
|
||||
formattedPath := formatPath(path)
|
||||
|
||||
route := &Route{
|
||||
Name: defaultName,
|
||||
Method: method,
|
||||
Subdomain: subdomain,
|
||||
tmpl: tmpl,
|
||||
Path: path,
|
||||
Handlers: handlers,
|
||||
FormattedPath: formattedPath,
|
||||
}
|
||||
return route, nil
|
||||
}
|
||||
|
||||
// Tmpl returns the path template, i
|
||||
// it contains the parsed template
|
||||
// for the route's path.
|
||||
// May contain zero named parameters.
|
||||
//
|
||||
// Developer can get his registered path
|
||||
// via Tmpl().Src, Route.Path is the path
|
||||
// converted to match the underline router's specs.
|
||||
func (r Route) Tmpl() macro.Template {
|
||||
return *r.tmpl
|
||||
}
|
||||
|
||||
// IsOnline returns true if the route is marked as "online" (state).
|
||||
func (r Route) IsOnline() bool {
|
||||
return r.Method != MethodNone
|
||||
}
|
||||
|
||||
// formats the parsed to the underline httprouter's path syntax.
|
||||
// path = "/api/users/:id"
|
||||
// return "/api/users/%v"
|
||||
//
|
||||
// path = "/files/*file"
|
||||
// return /files/%v
|
||||
//
|
||||
// path = "/:username/messages/:messageid"
|
||||
// return "/%v/messages/%v"
|
||||
// we don't care about performance here, it's prelisten.
|
||||
func formatPath(path string) string {
|
||||
if strings.Contains(path, ParamStart) || strings.Contains(path, WildcardParamStart) {
|
||||
var (
|
||||
startRune = ParamStart[0]
|
||||
wildcardStartRune = WildcardParamStart[0]
|
||||
)
|
||||
|
||||
var formattedParts []string
|
||||
parts := strings.Split(path, "/")
|
||||
for _, part := range parts {
|
||||
if len(part) == 0 {
|
||||
continue
|
||||
}
|
||||
if part[0] == startRune || part[0] == wildcardStartRune {
|
||||
// is param or wildcard param
|
||||
part = "%v"
|
||||
}
|
||||
formattedParts = append(formattedParts, part)
|
||||
}
|
||||
|
||||
return "/" + strings.Join(formattedParts, "/")
|
||||
}
|
||||
// the whole path is static just return it
|
||||
return path
|
||||
}
|
||||
|
||||
// ResolvePath returns the formatted path's %v replaced with the args.
|
||||
func (r Route) ResolvePath(args ...string) string {
|
||||
rpath, formattedPath := r.Path, r.FormattedPath
|
||||
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 rpath[len(rpath)-1] == WildcardParamStart[0] {
|
||||
parameter := strings.Join(args, "/")
|
||||
return fmt.Sprintf(formattedPath, parameter)
|
||||
}
|
||||
// else return the formattedPath with its args,
|
||||
// the order matters.
|
||||
for _, s := range args {
|
||||
formattedPath = strings.Replace(formattedPath, "%v", s, 1)
|
||||
}
|
||||
return formattedPath
|
||||
}
|
||||
156
core/router/router.go
Normal file
156
core/router/router.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/core/errors"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
mu sync.Mutex // for Downgrade, WrapRouter & BuildRouter,
|
||||
// not indeed but we don't to risk its usage by third-parties.
|
||||
requestHandler RequestHandler // build-accessible, can be changed to define a custom router or proxy, used on RefreshRouter too.
|
||||
mainHandler http.HandlerFunc // init-accessible
|
||||
wrapperFunc func(http.ResponseWriter, *http.Request, http.HandlerFunc)
|
||||
|
||||
cPool *context.Pool // used on RefreshRouter
|
||||
routesProvider RoutesProvider
|
||||
}
|
||||
|
||||
// NewRouter returns a new empty Router.
|
||||
func NewRouter() *Router { return &Router{} }
|
||||
|
||||
// RefreshRouter re-builds the router. Should be called when a route's state
|
||||
// changed (i.e Method changed at serve-time).
|
||||
func (router *Router) RefreshRouter() error {
|
||||
return router.BuildRouter(router.cPool, router.requestHandler, router.routesProvider)
|
||||
}
|
||||
|
||||
// BuildRouter builds the router based on
|
||||
// the context factory (explicit pool in this case),
|
||||
// the request handler which manages how the main handler will multiplexes the routes
|
||||
// provided by the third parameter, routerProvider (it's the api builder in this case) and
|
||||
// its wrapper.
|
||||
//
|
||||
// Use of RefreshRouter to re-build the router if needed.
|
||||
func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHandler, routesProvider RoutesProvider) error {
|
||||
|
||||
if requestHandler == nil {
|
||||
return errors.New("router: request handler is nil")
|
||||
}
|
||||
|
||||
if cPool == nil {
|
||||
return errors.New("router: context pool is nil")
|
||||
}
|
||||
|
||||
// build the handler using the routesProvider
|
||||
if err := requestHandler.Build(routesProvider); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
router.mu.Lock()
|
||||
defer router.mu.Unlock()
|
||||
|
||||
// store these for RefreshRouter's needs.
|
||||
router.cPool = cPool
|
||||
router.requestHandler = requestHandler
|
||||
router.routesProvider = routesProvider
|
||||
|
||||
// the important
|
||||
router.mainHandler = func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := cPool.Acquire(w, r)
|
||||
router.requestHandler.HandleRequest(ctx)
|
||||
cPool.Release(ctx)
|
||||
}
|
||||
|
||||
if router.wrapperFunc != nil { // if wrapper used then attach that as the router service
|
||||
router.mainHandler = NewWrapper(router.wrapperFunc, router.mainHandler).ServeHTTP
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Downgrade "downgrades", alters the router supervisor service(Router.mainHandler)
|
||||
// algorithm to a custom one,
|
||||
// be aware to change the global variables of 'ParamStart' and 'ParamWildcardStart'.
|
||||
// can be used to implement a custom proxy or
|
||||
// a custom router which should work with raw ResponseWriter, *Request
|
||||
// instead of the Context(which agaiin, can be retrieved by the Cramework's context pool).
|
||||
//
|
||||
// Note: Downgrade will by-pass the Wrapper, the caller is responsible for everything.
|
||||
// Downgrade is thread-safe.
|
||||
func (router *Router) Downgrade(newMainHandler http.HandlerFunc) {
|
||||
router.mu.Lock()
|
||||
router.mainHandler = newMainHandler
|
||||
router.mu.Unlock()
|
||||
}
|
||||
|
||||
// WrapRouter adds a wrapper on the top of the main router.
|
||||
// Usually it's useful for third-party middleware
|
||||
// when need to wrap the entire application with a middleware like CORS.
|
||||
//
|
||||
// Developers can add more than one wrappers,
|
||||
// those wrappers' execution comes from last to first.
|
||||
// That means that the second wrapper will wrap the first, and so on.
|
||||
//
|
||||
// Before build.
|
||||
func (router *Router) WrapRouter(wrapperFunc func(w http.ResponseWriter, r *http.Request, firstNextIsTheRouter http.HandlerFunc)) {
|
||||
router.mu.Lock()
|
||||
defer router.mu.Unlock()
|
||||
|
||||
if wrapperFunc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if router.wrapperFunc != nil {
|
||||
// wrap into one function, from bottom to top, end to begin.
|
||||
nextWrapper := wrapperFunc
|
||||
prevWrapper := router.wrapperFunc
|
||||
wrapperFunc = func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
if next != nil {
|
||||
nexthttpFunc := http.HandlerFunc(func(_w http.ResponseWriter, _r *http.Request) {
|
||||
prevWrapper(_w, _r, next)
|
||||
})
|
||||
nextWrapper(w, r, nexthttpFunc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
router.wrapperFunc = wrapperFunc
|
||||
}
|
||||
|
||||
// ServeHTTPC serves the raw context, useful if we have already a context, it by-pass the wrapper.
|
||||
func (router *Router) ServeHTTPC(ctx context.Context) {
|
||||
router.requestHandler.HandleRequest(ctx)
|
||||
}
|
||||
|
||||
func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
router.mainHandler(w, r)
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
router http.HandlerFunc // http.HandlerFunc to catch the CURRENT state of its .ServeHTTP on case of future change.
|
||||
wrapperFunc func(http.ResponseWriter, *http.Request, http.HandlerFunc)
|
||||
}
|
||||
|
||||
// NewWrapper returns a new http.Handler wrapped by the 'wrapperFunc'
|
||||
// the "next" is the final "wrapped" input parameter.
|
||||
//
|
||||
// Application is responsible to make it to work on more than one wrappers
|
||||
// via composition or func clojure.
|
||||
func NewWrapper(wrapperFunc func(w http.ResponseWriter, r *http.Request, routerNext http.HandlerFunc), wrapped http.HandlerFunc) http.Handler {
|
||||
return &wrapper{
|
||||
wrapperFunc: wrapperFunc,
|
||||
router: wrapped,
|
||||
}
|
||||
}
|
||||
|
||||
func (wr *wrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
wr.wrapperFunc(w, r, wr.router)
|
||||
}
|
||||
144
core/router/status.go
Normal file
144
core/router/status.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http" // just for status codes
|
||||
"sync"
|
||||
|
||||
"github.com/kataras/iris/context"
|
||||
)
|
||||
|
||||
// ErrorCodeHandler is the entry
|
||||
// of the list of all http error code handlers.
|
||||
type ErrorCodeHandler struct {
|
||||
StatusCode int
|
||||
Handler context.Handler
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Fire executes the specific an error http error status.
|
||||
// it's being wrapped to make sure that the handler
|
||||
// will render correctly.
|
||||
func (ch *ErrorCodeHandler) Fire(ctx context.Context) {
|
||||
// if we can reset the body
|
||||
if w, ok := ctx.IsRecording(); ok {
|
||||
// reset if previous content and it's recorder
|
||||
w.Reset()
|
||||
} else if w, ok := ctx.ResponseWriter().(*context.GzipResponseWriter); ok {
|
||||
// reset and disable the gzip in order to be an expected form of http error result
|
||||
w.ResetBody()
|
||||
w.Disable()
|
||||
} else {
|
||||
// if we can't reset the body and the body has been filled
|
||||
// which means that the status code already sent,
|
||||
// then do not fire this custom error code.
|
||||
if ctx.ResponseWriter().Written() != -1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
// ctx.StopExecution() // not uncomment this, is here to remember why to.
|
||||
// note for me: I don't stopping the execution of the other handlers
|
||||
// because may the user want to add a fallback error code
|
||||
// i.e
|
||||
// users := app.Party("/users")
|
||||
// users.Done(func(ctx context.Context){ if ctx.StatusCode() == 400 { /* custom error code for /users */ }})
|
||||
ch.Handler(ctx)
|
||||
}
|
||||
|
||||
func (ch *ErrorCodeHandler) updateHandler(h context.Handler) {
|
||||
ch.mu.Lock()
|
||||
ch.Handler = h
|
||||
ch.mu.Unlock()
|
||||
}
|
||||
|
||||
// ErrorCodeHandlers contains the http error code handlers.
|
||||
// User of this struct can register, get
|
||||
// a status code handler based on a status code or
|
||||
// fire based on a receiver context.
|
||||
type ErrorCodeHandlers struct {
|
||||
handlers []*ErrorCodeHandler
|
||||
}
|
||||
|
||||
func defaultErrorCodeHandlers() *ErrorCodeHandlers {
|
||||
chs := new(ErrorCodeHandlers)
|
||||
// register some common error handlers.
|
||||
// Note that they can be registered on-fly but
|
||||
// we don't want to reduce the performance even
|
||||
// on the first failed request.
|
||||
for _, statusCode := range []int{
|
||||
http.StatusNotFound,
|
||||
http.StatusMethodNotAllowed,
|
||||
http.StatusInternalServerError} {
|
||||
chs.Register(statusCode, statusText(statusCode))
|
||||
}
|
||||
|
||||
return chs
|
||||
}
|
||||
|
||||
func statusText(statusCode int) context.Handler {
|
||||
return func(ctx context.Context) {
|
||||
if _, err := ctx.WriteString(http.StatusText(statusCode)); err != nil {
|
||||
// ctx.Application().Log("(status code: %d) %s",
|
||||
// err.Error(), statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns an http error handler based on the "statusCode".
|
||||
// If not found it returns nil.
|
||||
func (s *ErrorCodeHandlers) Get(statusCode int) *ErrorCodeHandler {
|
||||
for i, n := 0, len(s.handlers); i < n; i++ {
|
||||
if h := s.handlers[i]; h.StatusCode == statusCode {
|
||||
return h
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register registers an error http status code
|
||||
// based on the "statusCode" >= 400.
|
||||
// The handler is being wrapepd by a generic
|
||||
// handler which will try to reset
|
||||
// the body if recorder was enabled
|
||||
// and/or disable the gzip if gzip response recorder
|
||||
// was active.
|
||||
func (s *ErrorCodeHandlers) Register(statusCode int, handler context.Handler) *ErrorCodeHandler {
|
||||
if statusCode < 400 {
|
||||
return nil
|
||||
}
|
||||
|
||||
h := s.Get(statusCode)
|
||||
if h == nil {
|
||||
ch := &ErrorCodeHandler{
|
||||
StatusCode: statusCode,
|
||||
Handler: handler,
|
||||
}
|
||||
s.handlers = append(s.handlers, ch)
|
||||
// create new and add it
|
||||
return ch
|
||||
}
|
||||
// otherwise update the handler
|
||||
h.updateHandler(handler)
|
||||
return h
|
||||
}
|
||||
|
||||
// Fire executes an error http status code handler
|
||||
// based on the context's status code.
|
||||
//
|
||||
// If a handler is not already registered,
|
||||
// then it creates & registers a new trivial handler on the-fly.
|
||||
func (s *ErrorCodeHandlers) Fire(ctx context.Context) {
|
||||
statusCode := ctx.GetStatusCode()
|
||||
if statusCode < 400 {
|
||||
return
|
||||
}
|
||||
ch := s.Get(statusCode)
|
||||
if ch == nil {
|
||||
ch = s.Register(statusCode, statusText(statusCode))
|
||||
}
|
||||
|
||||
ch.Fire(ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user