Use go embed for web files and remove httptreemux (#382)

- replace togo with go embed
- replace httptreemux with gin

closes #308
This commit is contained in:
Anbraten 2021-09-29 17:34:56 +02:00 committed by GitHub
parent a82d569bd1
commit ed6d3f3cea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 62 additions and 12984 deletions

8
.gitignore vendored
View file

@ -18,8 +18,9 @@
*.out
### Frontend ###
web/node_modules
web/dist/files
web/dist/**
!web/dist/.gitkeep
web/node_modules/
web/*.log
web/.env
@ -40,6 +41,3 @@ server/swagger/files/*.json
server/swagger/swagger_gen.go
docs/venv
dist/
node_modules/

View file

@ -46,7 +46,7 @@ test-server:
$(DOCKER_RUN) go test -race -timeout 30s github.com/woodpecker-ci/woodpecker/cmd/server
test-frontend:
(cd web/; yarn run test)
(cd web/; yarn; yarn run test)
test-lib:
$(DOCKER_RUN) go test -race -timeout 30s $(shell go list ./... | grep -v '/cmd/')
@ -60,7 +60,7 @@ build-server:
$(DOCKER_RUN) go build -o build/woodpecker-server github.com/woodpecker-ci/woodpecker/cmd/server
build-frontend:
(cd web/; yarn run build)
(cd web/; yarn; yarn run build)
build: build-agent build-server

View file

@ -18,6 +18,7 @@ import (
"fmt"
"time"
"github.com/gin-gonic/gin"
"github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/environments"
@ -37,7 +38,6 @@ import (
"github.com/woodpecker-ci/woodpecker/server/store/datastore"
"github.com/woodpecker-ci/woodpecker/server/web"
"github.com/dimfeld/httptreemux"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/sirupsen/logrus"
@ -201,8 +201,8 @@ func setupCoding(c *cli.Context) (remote.Remote, error) {
})
}
func setupTree(c *cli.Context) *httptreemux.ContextMux {
tree := httptreemux.NewContextMux()
func setupTree(c *cli.Context) *gin.Engine {
tree := gin.New()
web.New(
web.WithSync(time.Hour*72),
web.WithDocs(c.String("docs")),

1
go.mod
View file

@ -9,7 +9,6 @@ require (
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/bradrydzewski/togo v0.0.0-20180401185031-50a0e4726e74 // indirect
github.com/containerd/containerd v1.5.5 // indirect
github.com/dimfeld/httptreemux v5.0.1+incompatible
github.com/docker/cli v0.0.0-20200303215952-eb310fca4956
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v20.10.8+incompatible

2
go.sum
View file

@ -231,8 +231,6 @@ github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8l
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimfeld/httptreemux v5.0.1+incompatible h1:Qj3gVcDNoOthBAqftuD596rm4wg/adLLz5xh5CmpiCA=
github.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20200303215952-eb310fca4956 h1:5/ZRsUbguX7xFNLlbxVQY/yhD3Psy+vylKZrNme5BJs=
github.com/docker/cli v0.0.0-20200303215952-eb310fca4956/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=

View file

@ -25,15 +25,15 @@ import (
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/shared/token"
"github.com/woodpecker-ci/woodpecker/version"
"github.com/woodpecker-ci/woodpecker/web/dist"
"github.com/woodpecker-ci/woodpecker/web"
"github.com/dimfeld/httptreemux"
"github.com/gin-gonic/gin"
)
// Endpoint provides the website endpoints.
type Endpoint interface {
// Register registers the provider endpoints.
Register(*httptreemux.ContextMux)
Register(*gin.Engine)
}
// New returns the default website endpoint.
@ -44,10 +44,10 @@ func New(opt ...Option) Endpoint {
}
return &website{
fs: dist.New(),
fs: web.HttpFS(),
opts: opts,
tmpl: mustCreateTemplate(
string(dist.MustLookup("/index.html")),
string(web.MustLookup("index.html")),
),
}
}
@ -58,12 +58,12 @@ type website struct {
tmpl *template.Template
}
func (w *website) Register(mux *httptreemux.ContextMux) {
func (w *website) Register(mux *gin.Engine) {
h := http.FileServer(w.fs)
h = setupCache(h)
mux.Handler("GET", "/favicon.svg", h)
mux.Handler("GET", "/static/*filepath", h)
mux.NotFoundHandler = w.handleIndex
mux.GET("/favicon.svg", gin.WrapH(h))
mux.GET("/static/*filepath", gin.WrapH(h))
mux.NoRoute(gin.WrapF(w.handleIndex))
}
func (w *website) handleIndex(rw http.ResponseWriter, r *http.Request) {

View file

@ -1,23 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test

View file

@ -1,14 +0,0 @@
language: go
gobuild_args: "-v -race"
go:
- 1.5
- 1.6
- 1.7
- 1.8
- 1.9
- tip
matrix:
allow_failures:
- go: tip

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014,2015 Daniel Imfeld
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,240 +0,0 @@
httptreemux [![Build Status](https://travis-ci.org/dimfeld/httptreemux.png?branch=master)](https://travis-ci.org/dimfeld/httptreemux) [![GoDoc](https://godoc.org/github.com/dimfeld/httptreemux?status.svg)](https://godoc.org/github.com/dimfeld/httptreemux)
===========
High-speed, flexible, tree-based HTTP router for Go.
This is inspired by [Julien Schmidt's httprouter](https://www.github.com/julienschmidt/httprouter), in that it uses a patricia tree, but the implementation is rather different. Specifically, the routing rules are relaxed so that a single path segment may be a wildcard in one route and a static token in another. This gives a nice combination of high performance with a lot of convenience in designing the routing patterns. In [benchmarks](https://github.com/julienschmidt/go-http-routing-benchmark), httptreemux is close to, but slightly slower than, httprouter.
Release notes may be found using the [Github releases tab](https://github.com/dimfeld/httptreemux/releases). Version numbers are compatible with the [Semantic Versioning 2.0.0](http://semver.org/) convention, and a new release is made after every change to the code.
## Why?
There are a lot of good routers out there. But looking at the ones that were really lightweight, I couldn't quite get something that fit with the route patterns I wanted. The code itself is simple enough, so I spent an evening writing this.
## Handler
The handler is a simple function with the prototype `func(w http.ResponseWriter, r *http.Request, params map[string]string)`. The params argument contains the parameters parsed from wildcards and catch-alls in the URL, as described below. This type is aliased as httptreemux.HandlerFunc.
### Using http.HandlerFunc
Due to the inclusion of the [context](https://godoc.org/context) package as of Go 1.7, `httptreemux` now supports handlers of type [http.HandlerFunc](https://godoc.org/net/http#HandlerFunc). There are two ways to enable this support.
#### Adapting an Existing Router
The `UsingContext` method will wrap the router or group in a new group at the same path, but adapted for use with `context` and `http.HandlerFunc`.
```go
router := httptreemux.New()
group := router.NewGroup("/api")
group.GET("/v1/:id", func(w http.ResponseWriter, r *http.Request, params map[string]string) {
id := params["id"]
fmt.Fprintf(w, "GET /api/v1/%s", id)
})
// UsingContext returns a version of the router or group with context support.
ctxGroup := group.UsingContext() // sibling to 'group' node in tree
ctxGroup.GET("/v2/:id", func(w http.ResponseWriter, r *http.Request) {
params := httptreemux.ContextParams(r.Context())
id := params["id"]
fmt.Fprintf(w, "GET /api/v2/%s", id)
})
http.ListenAndServe(":8080", router)
```
#### New Router with Context Support
The `NewContextMux` function returns a router preconfigured for use with `context` and `http.HandlerFunc`.
```go
router := httptreemux.NewContextMux()
router.GET("/:page", func(w http.ResponseWriter, r *http.Request) {
params := httptreemux.ContextParams(r.Context())
fmt.Fprintf(w, "GET /%s", params["page"])
})
group := tree.NewGroup("/api")
group.GET("/v1/:id", func(w http.ResponseWriter, r *http.Request) {
params := httptreemux.ContextParams(r.Context())
id := params["id"]
fmt.Fprintf(w, "GET /api/v1/%s", id)
})
http.ListenAndServe(":8080", router)
```
## Routing Rules
The syntax here is also modeled after httprouter. Each variable in a path may match on one segment only, except for an optional catch-all variable at the end of the URL.
Some examples of valid URL patterns are:
* `/post/all`
* `/post/:postid`
* `/post/:postid/page/:page`
* `/post/:postid/:page`
* `/images/*path`
* `/favicon.ico`
* `/:year/:month/`
* `/:year/:month/:post`
* `/:page`
Note that all of the above URL patterns may exist concurrently in the router.
Path elements starting with `:` indicate a wildcard in the path. A wildcard will only match on a single path segment. That is, the pattern `/post/:postid` will match on `/post/1` or `/post/1/`, but not `/post/1/2`.
A path element starting with `*` is a catch-all, whose value will be a string containing all text in the URL matched by the wildcards. For example, with a pattern of `/images/*path` and a requested URL `images/abc/def`, path would contain `abc/def`.
#### Using : and * in routing patterns
The characters `:` and `*` can be used at the beginning of a path segment by escaping them with a backslash. A double backslash at the beginning of a segment is interpreted as a single backslash. These escapes are only checked at the very beginning of a path segment; they are not necessary or processed elsewhere in a token.
```go
router.GET("/foo/\\*starToken", handler) // matches /foo/*starToken
router.GET("/foo/star*inTheMiddle", handler) // matches /foo/star*inTheMiddle
router.GET("/foo/starBackslash\\*", handler) // matches /foo/starBackslash\*
router.GET("/foo/\\\\*backslashWithStar") // matches /foo/\*backslashWithStar
```
### Routing Groups
Lets you create a new group of routes with a given path prefix. Makes it easier to create clusters of paths like:
* `/api/v1/foo`
* `/api/v1/bar`
To use this you do:
```go
router = httptreemux.New()
api := router.NewGroup("/api/v1")
api.GET("/foo", fooHandler) // becomes /api/v1/foo
api.GET("/bar", barHandler) // becomes /api/v1/bar
```
### Routing Priority
The priority rules in the router are simple.
1. Static path segments take the highest priority. If a segment and its subtree are able to match the URL, that match is returned.
2. Wildcards take second priority. For a particular wildcard to match, that wildcard and its subtree must match the URL.
3. Finally, a catch-all rule will match when the earlier path segments have matched, and none of the static or wildcard conditions have matched. Catch-all rules must be at the end of a pattern.
So with the following patterns adapted from [simpleblog](https://www.github.com/dimfeld/simpleblog), we'll see certain matches:
```go
router = httptreemux.New()
router.GET("/:page", pageHandler)
router.GET("/:year/:month/:post", postHandler)
router.GET("/:year/:month", archiveHandler)
router.GET("/images/*path", staticHandler)
router.GET("/favicon.ico", staticHandler)
```
#### Example scenarios
- `/abc` will match `/:page`
- `/2014/05` will match `/:year/:month`
- `/2014/05/really-great-blog-post` will match `/:year/:month/:post`
- `/images/CoolImage.gif` will match `/images/*path`
- `/images/2014/05/MayImage.jpg` will also match `/images/*path`, with all the text after `/images` stored in the variable path.
- `/favicon.ico` will match `/favicon.ico`
### Special Method Behavior
If TreeMux.HeadCanUseGet is set to true, the router will call the GET handler for a pattern when a HEAD request is processed, if no HEAD handler has been added for that pattern. This behavior is enabled by default.
Go's http.ServeContent and related functions already handle the HEAD method correctly by sending only the header, so in most cases your handlers will not need any special cases for it.
By default TreeMux.OptionsHandler is a null handler that doesn't affect your routing. If you set the handler, it will be called on OPTIONS requests to a path already registered by another method. If you set a path specific handler by using `router.OPTIONS`, it will override the global Options Handler for that path.
### Trailing Slashes
The router has special handling for paths with trailing slashes. If a pattern is added to the router with a trailing slash, any matches on that pattern without a trailing slash will be redirected to the version with the slash. If a pattern does not have a trailing slash, matches on that pattern with a trailing slash will be redirected to the version without.
The trailing slash flag is only stored once for a pattern. That is, if a pattern is added for a method with a trailing slash, all other methods for that pattern will also be considered to have a trailing slash, regardless of whether or not it is specified for those methods too.
However this behavior can be turned off by setting TreeMux.RedirectTrailingSlash to false. By default it is set to true.
One exception to this rule is catch-all patterns. By default, trailing slash redirection is disabled on catch-all patterns, since the structure of the entire URL and the desired patterns can not be predicted. If trailing slash removal is desired on catch-all patterns, set TreeMux.RemoveCatchAllTrailingSlash to true.
```go
router = httptreemux.New()
router.GET("/about", pageHandler)
router.GET("/posts/", postIndexHandler)
router.POST("/posts", postFormHandler)
GET /about will match normally.
GET /about/ will redirect to /about.
GET /posts will redirect to /posts/.
GET /posts/ will match normally.
POST /posts will redirect to /posts/, because the GET method used a trailing slash.
```
### Custom Redirects
RedirectBehavior sets the behavior when the router redirects the request to the canonical version of the requested URL using RedirectTrailingSlash or RedirectClean. The default behavior is to return a 301 status, redirecting the browser to the version of the URL that matches the given pattern.
These are the values accepted for RedirectBehavior. You may also add these values to the RedirectMethodBehavior map to define custom per-method redirect behavior.
* Redirect301 - HTTP 301 Moved Permanently; this is the default.
* Redirect307 - HTTP/1.1 Temporary Redirect
* Redirect308 - RFC7538 Permanent Redirect
* UseHandler - Don't redirect to the canonical path. Just call the handler instead.
#### Rationale/Usage
On a POST request, most browsers that receive a 301 will submit a GET request to the redirected URL, meaning that any data will likely be lost. If you want to handle and avoid this behavior, you may use Redirect307, which causes most browsers to resubmit the request using the original method and request body.
Since 307 is supposed to be a temporary redirect, the new 308 status code has been proposed, which is treated the same, except it indicates correctly that the redirection is permanent. The big caveat here is that the RFC is relatively recent, and older or non-compliant browsers will not handle it. Therefore its use is not recommended unless you really know what you're doing.
Finally, the UseHandler value will simply call the handler function for the pattern, without redirecting to the canonical version of the URL.
### RequestURI vs. URL.Path
#### Escaped Slashes
Go automatically processes escaped characters in a URL, converting + to a space and %XX to the corresponding character. This can present issues when the URL contains a %2f, which is unescaped to '/'. This isn't an issue for most applications, but it will prevent the router from correctly matching paths and wildcards.
For example, the pattern `/post/:post` would not match on `/post/abc%2fdef`, which is unescaped to `/post/abc/def`. The desired behavior is that it matches, and the `post` wildcard is set to `abc/def`.
Therefore, this router defaults to using the raw URL, stored in the Request.RequestURI variable. Matching wildcards and catch-alls are then unescaped, to give the desired behavior.
TL;DR: If a requested URL contains a %2f, this router will still do the right thing. Some Go HTTP routers may not due to [Go issue 3659](https://code.google.com/p/go/issues/detail?id=3659).
#### Escaped Characters
As mentioned above, characters in the URL are not unescaped when using RequestURI to determine the matched route. If this is a problem for you and you are unable to switch to URL.Path for the above reasons, you may set `router.EscapeAddedRoutes` to `true`. This option will run each added route through the `URL.EscapedPath` function, and add an additional route if the escaped version differs.
#### http Package Utility Functions
Although using RequestURI avoids the issue described above, certain utility functions such as `http.StripPrefix` modify URL.Path, and expect that the underlying router is using that field to make its decision. If you are using some of these functions, set the router's `PathSource` member to `URLPath`. This will give up the proper handling of escaped slashes described above, while allowing the router to work properly with these utility functions.
## Concurrency
The router contains an `RWMutex` that arbitrates access to the tree. This allows routes to be safely added from multiple goroutines at once.
No concurrency controls are needed when only reading from the tree, so the default behavior is to not use the `RWMutex` when serving a request. This avoids a theoretical slowdown under high-usage scenarios from competing atomic integer operations inside the `RWMutex`. If your application adds routes to the router after it has begun serving requests, you should avoid potential race conditions by setting `router.SafeAddRoutesWhileRunning` to `true` to use the `RWMutex` when serving requests.
## Error Handlers
### NotFoundHandler
TreeMux.NotFoundHandler can be set to provide custom 404-error handling. The default implementation is Go's `http.NotFound` function.
### MethodNotAllowedHandler
If a pattern matches, but the pattern does not have an associated handler for the requested method, the router calls the MethodNotAllowedHandler. The default
version of this handler just writes the status code `http.StatusMethodNotAllowed` and sets the response header's `Allowed` field appropriately.
### Panic Handling
TreeMux.PanicHandler can be set to provide custom panic handling. The `SimplePanicHandler` just writes the status code `http.StatusInternalServerError`. The function `ShowErrorsPanicHandler`, adapted from [gocraft/web](https://github.com/gocraft/web), will print panic errors to the browser in an easily-readable format.
## Unexpected Differences from Other Routers
This router is intentionally light on features in the name of simplicity and
performance. When coming from another router that does heavier processing behind
the scenes, you may encounter some unexpected behavior. This list is by no means
exhaustive, but covers some nonobvious cases that users have encountered.
### gorilla/pat query string modifications
When matching on parameters in a route, the `gorilla/pat` router will modify
`Request.URL.RawQuery` to make it appear like the parameters were in the
query string. `httptreemux` does not do this. See [Issue #26](https://github.com/dimfeld/httptreemux/issues/26) for more details and a
code snippet that can perform this transformation for you, should you want it.
## Middleware
This package provides no middleware. But there are a lot of great options out there and it's pretty easy to write your own.
# Acknowledgements
* Inspiration from Julien Schmidt's [httprouter](https://github.com/julienschmidt/httprouter)
* Show Errors panic handler from [gocraft/web](https://github.com/gocraft/web)

View file

@ -1,123 +0,0 @@
// +build go1.7
package httptreemux
import (
"context"
"net/http"
)
// ContextGroup is a wrapper around Group, with the purpose of mimicking its API, but with the use of http.HandlerFunc-based handlers.
// Instead of passing a parameter map via the handler (i.e. httptreemux.HandlerFunc), the path parameters are accessed via the request
// object's context.
type ContextGroup struct {
group *Group
}
// UsingContext wraps the receiver to return a new instance of a ContextGroup.
// The returned ContextGroup is a sibling to its wrapped Group, within the parent TreeMux.
// The choice of using a *Group as the receiver, as opposed to a function parameter, allows chaining
// while method calls between a TreeMux, Group, and ContextGroup. For example:
//
// tree := httptreemux.New()
// group := tree.NewGroup("/api")
//
// group.GET("/v1", func(w http.ResponseWriter, r *http.Request, params map[string]string) {
// w.Write([]byte(`GET /api/v1`))
// })
//
// group.UsingContext().GET("/v2", func(w http.ResponseWriter, r *http.Request) {
// w.Write([]byte(`GET /api/v2`))
// })
//
// http.ListenAndServe(":8080", tree)
//
func (g *Group) UsingContext() *ContextGroup {
return &ContextGroup{g}
}
// NewContextGroup adds a child context group to its path.
func (cg *ContextGroup) NewContextGroup(path string) *ContextGroup {
return &ContextGroup{cg.group.NewGroup(path)}
}
func (cg *ContextGroup) NewGroup(path string) *ContextGroup {
return cg.NewContextGroup(path)
}
// Handle allows handling HTTP requests via an http.HandlerFunc, as opposed to an httptreemux.HandlerFunc.
// Any parameters from the request URL are stored in a map[string]string in the request's context.
func (cg *ContextGroup) Handle(method, path string, handler http.HandlerFunc) {
cg.group.Handle(method, path, func(w http.ResponseWriter, r *http.Request, params map[string]string) {
if params != nil {
r = r.WithContext(AddParamsToContext(r.Context(), params))
}
handler(w, r)
})
}
// Handler allows handling HTTP requests via an http.Handler interface, as opposed to an httptreemux.HandlerFunc.
// Any parameters from the request URL are stored in a map[string]string in the request's context.
func (cg *ContextGroup) Handler(method, path string, handler http.Handler) {
cg.group.Handle(method, path, func(w http.ResponseWriter, r *http.Request, params map[string]string) {
if params != nil {
r = r.WithContext(AddParamsToContext(r.Context(), params))
}
handler.ServeHTTP(w, r)
})
}
// GET is convenience method for handling GET requests on a context group.
func (cg *ContextGroup) GET(path string, handler http.HandlerFunc) {
cg.Handle("GET", path, handler)
}
// POST is convenience method for handling POST requests on a context group.
func (cg *ContextGroup) POST(path string, handler http.HandlerFunc) {
cg.Handle("POST", path, handler)
}
// PUT is convenience method for handling PUT requests on a context group.
func (cg *ContextGroup) PUT(path string, handler http.HandlerFunc) {
cg.Handle("PUT", path, handler)
}
// DELETE is convenience method for handling DELETE requests on a context group.
func (cg *ContextGroup) DELETE(path string, handler http.HandlerFunc) {
cg.Handle("DELETE", path, handler)
}
// PATCH is convenience method for handling PATCH requests on a context group.
func (cg *ContextGroup) PATCH(path string, handler http.HandlerFunc) {
cg.Handle("PATCH", path, handler)
}
// HEAD is convenience method for handling HEAD requests on a context group.
func (cg *ContextGroup) HEAD(path string, handler http.HandlerFunc) {
cg.Handle("HEAD", path, handler)
}
// OPTIONS is convenience method for handling OPTIONS requests on a context group.
func (cg *ContextGroup) OPTIONS(path string, handler http.HandlerFunc) {
cg.Handle("OPTIONS", path, handler)
}
// ContextParams returns the params map associated with the given context if one exists. Otherwise, an empty map is returned.
func ContextParams(ctx context.Context) map[string]string {
if p, ok := ctx.Value(paramsContextKey).(map[string]string); ok {
return p
}
return map[string]string{}
}
// AddParamsToContext inserts a parameters map into a context using
// the package's internal context key. Clients of this package should
// really only use this for unit tests.
func AddParamsToContext(ctx context.Context, params map[string]string) context.Context {
return context.WithValue(ctx, paramsContextKey, params)
}
type contextKey int
// paramsContextKey is used to retrieve a path's params map from a request's context.
const paramsContextKey contextKey = 0

View file

@ -1,195 +0,0 @@
package httptreemux
import (
"fmt"
"net/url"
"strings"
)
type Group struct {
path string
mux *TreeMux
}
// Add a sub-group to this group
func (g *Group) NewGroup(path string) *Group {
if len(path) < 1 {
panic("Group path must not be empty")
}
checkPath(path)
path = g.path + path
//Don't want trailing slash as all sub-paths start with slash
if path[len(path)-1] == '/' {
path = path[:len(path)-1]
}
return &Group{path, g.mux}
}
// Path elements starting with : indicate a wildcard in the path. A wildcard will only match on a
// single path segment. That is, the pattern `/post/:postid` will match on `/post/1` or `/post/1/`,
// but not `/post/1/2`.
//
// A path element starting with * is a catch-all, whose value will be a string containing all text
// in the URL matched by the wildcards. For example, with a pattern of `/images/*path` and a
// requested URL `images/abc/def`, path would contain `abc/def`.
//
// # Routing Rule Priority
//
// The priority rules in the router are simple.
//
// 1. Static path segments take the highest priority. If a segment and its subtree are able to match the URL, that match is returned.
//
// 2. Wildcards take second priority. For a particular wildcard to match, that wildcard and its subtree must match the URL.
//
// 3. Finally, a catch-all rule will match when the earlier path segments have matched, and none of the static or wildcard conditions have matched. Catch-all rules must be at the end of a pattern.
//
// So with the following patterns, we'll see certain matches:
// router = httptreemux.New()
// router.GET("/:page", pageHandler)
// router.GET("/:year/:month/:post", postHandler)
// router.GET("/:year/:month", archiveHandler)
// router.GET("/images/*path", staticHandler)
// router.GET("/favicon.ico", staticHandler)
//
// /abc will match /:page
// /2014/05 will match /:year/:month
// /2014/05/really-great-blog-post will match /:year/:month/:post
// /images/CoolImage.gif will match /images/*path
// /images/2014/05/MayImage.jpg will also match /images/*path, with all the text after /images stored in the variable path.
// /favicon.ico will match /favicon.ico
//
// # Trailing Slashes
//
// The router has special handling for paths with trailing slashes. If a pattern is added to the
// router with a trailing slash, any matches on that pattern without a trailing slash will be
// redirected to the version with the slash. If a pattern does not have a trailing slash, matches on
// that pattern with a trailing slash will be redirected to the version without.
//
// The trailing slash flag is only stored once for a pattern. That is, if a pattern is added for a
// method with a trailing slash, all other methods for that pattern will also be considered to have a
// trailing slash, regardless of whether or not it is specified for those methods too.
//
// This behavior can be turned off by setting TreeMux.RedirectTrailingSlash to false. By
// default it is set to true. The specifics of the redirect depend on RedirectBehavior.
//
// One exception to this rule is catch-all patterns. By default, trailing slash redirection is
// disabled on catch-all patterns, since the structure of the entire URL and the desired patterns
// can not be predicted. If trailing slash removal is desired on catch-all patterns, set
// TreeMux.RemoveCatchAllTrailingSlash to true.
//
// router = httptreemux.New()
// router.GET("/about", pageHandler)
// router.GET("/posts/", postIndexHandler)
// router.POST("/posts", postFormHandler)
//
// GET /about will match normally.
// GET /about/ will redirect to /about.
// GET /posts will redirect to /posts/.
// GET /posts/ will match normally.
// POST /posts will redirect to /posts/, because the GET method used a trailing slash.
func (g *Group) Handle(method string, path string, handler HandlerFunc) {
g.mux.mutex.Lock()
defer g.mux.mutex.Unlock()
addSlash := false
addOne := func(thePath string) {
node := g.mux.root.addPath(thePath[1:], nil, false)
if addSlash {
node.addSlash = true
}
node.setHandler(method, handler, false)
if g.mux.HeadCanUseGet && method == "GET" && node.leafHandler["HEAD"] == nil {
node.setHandler("HEAD", handler, true)
}
}
checkPath(path)
path = g.path + path
if len(path) == 0 {
panic("Cannot map an empty path")
}
if len(path) > 1 && path[len(path)-1] == '/' && g.mux.RedirectTrailingSlash {
addSlash = true
path = path[:len(path)-1]
}
if g.mux.EscapeAddedRoutes {
u, err := url.ParseRequestURI(path)
if err != nil {
panic("URL parsing error " + err.Error() + " on url " + path)
}
escapedPath := unescapeSpecial(u.String())
if escapedPath != path {
addOne(escapedPath)
}
}
addOne(path)
}
// Syntactic sugar for Handle("GET", path, handler)
func (g *Group) GET(path string, handler HandlerFunc) {
g.Handle("GET", path, handler)
}
// Syntactic sugar for Handle("POST", path, handler)
func (g *Group) POST(path string, handler HandlerFunc) {
g.Handle("POST", path, handler)
}
// Syntactic sugar for Handle("PUT", path, handler)
func (g *Group) PUT(path string, handler HandlerFunc) {
g.Handle("PUT", path, handler)
}
// Syntactic sugar for Handle("DELETE", path, handler)
func (g *Group) DELETE(path string, handler HandlerFunc) {
g.Handle("DELETE", path, handler)
}
// Syntactic sugar for Handle("PATCH", path, handler)
func (g *Group) PATCH(path string, handler HandlerFunc) {
g.Handle("PATCH", path, handler)
}
// Syntactic sugar for Handle("HEAD", path, handler)
func (g *Group) HEAD(path string, handler HandlerFunc) {
g.Handle("HEAD", path, handler)
}
// Syntactic sugar for Handle("OPTIONS", path, handler)
func (g *Group) OPTIONS(path string, handler HandlerFunc) {
g.Handle("OPTIONS", path, handler)
}
func checkPath(path string) {
// All non-empty paths must start with a slash
if len(path) > 0 && path[0] != '/' {
panic(fmt.Sprintf("Path %s must start with slash", path))
}
}
func unescapeSpecial(s string) string {
// Look for sequences of \*, *, and \: that were escaped, and undo some of that escaping.
// Unescape /* since it references a wildcard token.
s = strings.Replace(s, "/%2A", "/*", -1)
// Unescape /\: since it references a literal colon
s = strings.Replace(s, "/%5C:", "/\\:", -1)
// Replace escaped /\\: with /\:
s = strings.Replace(s, "/%5C%5C:", "/%5C:", -1)
// Replace escaped /\* with /*
s = strings.Replace(s, "/%5C%2A", "/%2A", -1)
// Replace escaped /\\* with /\*
s = strings.Replace(s, "/%5C%5C%2A", "/%5C%2A", -1)
return s
}

View file

@ -1,211 +0,0 @@
package httptreemux
import (
"bufio"
"encoding/json"
"html/template"
"net/http"
"os"
"runtime"
"strings"
)
// SimplePanicHandler just returns error 500.
func SimplePanicHandler(w http.ResponseWriter, r *http.Request, err interface{}) {
w.WriteHeader(http.StatusInternalServerError)
}
// ShowErrorsPanicHandler prints a nice representation of an error to the browser.
// This was taken from github.com/gocraft/web, which adapted it from the Traffic project.
func ShowErrorsPanicHandler(w http.ResponseWriter, r *http.Request, err interface{}) {
const size = 4096
stack := make([]byte, size)
stack = stack[:runtime.Stack(stack, false)]
renderPrettyError(w, r, err, stack)
}
func makeErrorData(r *http.Request, err interface{}, stack []byte, filePath string, line int) map[string]interface{} {
data := map[string]interface{}{
"Stack": string(stack),
"Params": r.URL.Query(),
"Method": r.Method,
"FilePath": filePath,
"Line": line,
"Lines": readErrorFileLines(filePath, line),
}
if e, ok := err.(error); ok {
data["Error"] = e.Error()
} else {
data["Error"] = err
}
return data
}
func renderPrettyError(rw http.ResponseWriter, req *http.Request, err interface{}, stack []byte) {
_, filePath, line, _ := runtime.Caller(5)
data := makeErrorData(req, err, stack, filePath, line)
rw.Header().Set("Content-Type", "text/html")
rw.WriteHeader(http.StatusInternalServerError)
tpl := template.Must(template.New("ErrorPage").Parse(panicPageTpl))
tpl.Execute(rw, data)
}
func ShowErrorsJsonPanicHandler(w http.ResponseWriter, r *http.Request, err interface{}) {
const size = 4096
stack := make([]byte, size)
stack = stack[:runtime.Stack(stack, false)]
_, filePath, line, _ := runtime.Caller(4)
data := makeErrorData(r, err, stack, filePath, line)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(data)
}
func readErrorFileLines(filePath string, errorLine int) map[int]string {
lines := make(map[int]string)
file, err := os.Open(filePath)
if err != nil {
return lines
}
defer file.Close()
reader := bufio.NewReader(file)
currentLine := 0
for {
line, err := reader.ReadString('\n')
if err != nil || currentLine > errorLine+5 {
break
}
currentLine++
if currentLine >= errorLine-5 {
lines[currentLine] = strings.Replace(line, "\n", "", -1)
}
}
return lines
}
const panicPageTpl string = `
<html>
<head>
<title>Panic</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
html, body{ padding: 0; margin: 0; }
header { background: #C52F24; color: white; border-bottom: 2px solid #9C0606; }
h1 { padding: 10px 0; margin: 0; }
.container { margin: 0 20px; }
.error { font-size: 18px; background: #FFCCCC; color: #9C0606; padding: 10px 0; }
.file-info .file-name { font-weight: bold; }
.stack { height: 300px; overflow-y: scroll; border: 1px solid #e5e5e5; padding: 10px; }
table.source {
width: 100%;
border-collapse: collapse;
border: 1px solid #e5e5e5;
}
table.source td {
padding: 0;
}
table.source .numbers {
font-size: 14px;
vertical-align: top;
width: 1%;
color: rgba(0,0,0,0.3);
text-align: right;
}
table.source .numbers .number {
display: block;
padding: 0 5px;
border-right: 1px solid #e5e5e5;
}
table.source .numbers .number.line-{{ .Line }} {
border-right: 1px solid #ffcccc;
}
table.source .numbers pre {
white-space: pre-wrap;
}
table.source .code {
font-size: 14px;
vertical-align: top;
}
table.source .code .line {
padding-left: 10px;
display: block;
}
table.source .numbers .number,
table.source .code .line {
padding-top: 1px;
padding-bottom: 1px;
}
table.source .code .line:hover {
background-color: #f6f6f6;
}
table.source .line-{{ .Line }},
table.source line-{{ .Line }},
table.source .code .line.line-{{ .Line }}:hover {
background: #ffcccc;
}
</style>
</head>
<body>
<header>
<div class="container">
<h1>Error</h1>
</div>
</header>
<div class="error">
<p class="container">{{ .Error }}</p>
</div>
<div class="container">
<p class="file-info">
In <span class="file-name">{{ .FilePath }}:{{ .Line }}</span></p>
</p>
<table class="source">
<tr>
<td class="numbers">
<pre>{{ range $lineNumber, $line := .Lines }}<span class="number line-{{ $lineNumber }}">{{ $lineNumber }}</span>{{ end }}</pre>
</td>
<td class="code">
<pre>{{ range $lineNumber, $line := .Lines }}<span class="line line-{{ $lineNumber }}">{{ $line }}<br /></span>{{ end }}</pre>
</td>
</tr>
</table>
<h2>Stack</h2>
<pre class="stack">{{ .Stack }}</pre>
<h2>Request</h2>
<p><strong>Method:</strong> {{ .Method }}</p>
<h3>Parameters:</h3>
<ul>
{{ range $key, $value := .Params }}
<li><strong>{{ $key }}:</strong> {{ $value }}</li>
{{ end }}
</ul>
</div>
</body>
</html>
`

View file

@ -1,127 +0,0 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Based on the path package, Copyright 2009 The Go Authors.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
package httptreemux
// Clean is the URL version of path.Clean, it returns a canonical URL path
// for p, eliminating . and .. elements.
//
// The following rules are applied iteratively until no further processing can
// be done:
// 1. Replace multiple slashes with a single slash.
// 2. Eliminate each . path name element (the current directory).
// 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.
//
// If the result of this process is an empty string, "/" is returned
func Clean(p string) string {
if p == "" {
return "/"
}
n := len(p)
var buf []byte
// Invariants:
// reading from path; r is index of next byte to process.
// writing to buf; w is index of next byte to write.
// path must start with '/'
r := 1
w := 1
if p[0] != '/' {
r = 0
buf = make([]byte, n+1)
buf[0] = '/'
}
trailing := n > 2 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp). So in contrast to the path package this
// loop has no expensive function calls (except 1x make)
for r < n {
switch {
case p[r] == '/':
// empty path element, trailing slash is added after the end
r++
case p[r] == '.' && r+1 == n:
trailing = true
r++
case p[r] == '.' && p[r+1] == '/':
// . element
r++
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// .. element: remove to last /
r += 2
if w > 1 {
// can backtrack
w--
if buf == nil {
for w > 1 && p[w] != '/' {
w--
}
} else {
for w > 1 && buf[w] != '/' {
w--
}
}
}
default:
// real path element.
// add slash if needed
if w > 1 {
bufApp(&buf, p, w, '/')
w++
}
// copy element
for r < n && p[r] != '/' {
bufApp(&buf, p, w, p[r])
w++
r++
}
}
}
// re-append trailing slash
if trailing && w > 1 {
bufApp(&buf, p, w, '/')
w++
}
// Turn empty string into "/"
if w == 0 {
return "/"
}
if buf == nil {
return p[:w]
}
return string(buf[:w])
}
// internal helper to lazily create a buffer if necessary
func bufApp(buf *[]byte, s string, w int, c byte) {
if *buf == nil {
if s[w] == c {
return
}
*buf = make([]byte, len(s))
copy(*buf, s[:w])
}
(*buf)[w] = c
}

View file

@ -1,300 +0,0 @@
// This is inspired by Julien Schmidt's httprouter, in that it uses a patricia tree, but the
// implementation is rather different. Specifically, the routing rules are relaxed so that a
// single path segment may be a wildcard in one route and a static token in another. This gives a
// nice combination of high performance with a lot of convenience in designing the routing patterns.
package httptreemux
import (
"fmt"
"net/http"
"net/url"
)
// The params argument contains the parameters parsed from wildcards and catch-alls in the URL.
type HandlerFunc func(http.ResponseWriter, *http.Request, map[string]string)
type PanicHandler func(http.ResponseWriter, *http.Request, interface{})
// RedirectBehavior sets the behavior when the router redirects the request to the
// canonical version of the requested URL using RedirectTrailingSlash or RedirectClean.
// The default behavior is to return a 301 status, redirecting the browser to the version
// of the URL that matches the given pattern.
//
// On a POST request, most browsers that receive a 301 will submit a GET request to
// the redirected URL, meaning that any data will likely be lost. If you want to handle
// and avoid this behavior, you may use Redirect307, which causes most browsers to
// resubmit the request using the original method and request body.
//
// Since 307 is supposed to be a temporary redirect, the new 308 status code has been
// proposed, which is treated the same, except it indicates correctly that the redirection
// is permanent. The big caveat here is that the RFC is relatively recent, and older
// browsers will not know what to do with it. Therefore its use is not recommended
// unless you really know what you're doing.
//
// Finally, the UseHandler value will simply call the handler function for the pattern.
type RedirectBehavior int
type PathSource int
const (
Redirect301 RedirectBehavior = iota // Return 301 Moved Permanently
Redirect307 // Return 307 HTTP/1.1 Temporary Redirect
Redirect308 // Return a 308 RFC7538 Permanent Redirect
UseHandler // Just call the handler function
RequestURI PathSource = iota // Use r.RequestURI
URLPath // Use r.URL.Path
)
// LookupResult contains information about a route lookup, which is returned from Lookup and
// can be passed to ServeLookupResult if the request should be served.
type LookupResult struct {
// StatusCode informs the caller about the result of the lookup.
// This will generally be `http.StatusNotFound` or `http.StatusMethodNotAllowed` for an
// error case. On a normal success, the statusCode will be `http.StatusOK`. A redirect code
// will also be used in the case
StatusCode int
handler HandlerFunc
params map[string]string
leafHandler map[string]HandlerFunc // Only has a value when StatusCode is MethodNotAllowed.
}
// Dump returns a text representation of the routing tree.
func (t *TreeMux) Dump() string {
return t.root.dumpTree("", "")
}
func (t *TreeMux) serveHTTPPanic(w http.ResponseWriter, r *http.Request) {
if err := recover(); err != nil {
t.PanicHandler(w, r, err)
}
}
func (t *TreeMux) redirectStatusCode(method string) (int, bool) {
var behavior RedirectBehavior
var ok bool
if behavior, ok = t.RedirectMethodBehavior[method]; !ok {
behavior = t.RedirectBehavior
}
switch behavior {
case Redirect301:
return http.StatusMovedPermanently, true
case Redirect307:
return http.StatusTemporaryRedirect, true
case Redirect308:
// Go doesn't have a constant for this yet. Yet another sign
// that you probably shouldn't use it.
return 308, true
case UseHandler:
return 0, false
default:
return http.StatusMovedPermanently, true
}
}
func redirectHandler(newPath string, statusCode int) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, params map[string]string) {
redirect(w, r, newPath, statusCode)
}
}
func redirect(w http.ResponseWriter, r *http.Request, newPath string, statusCode int) {
newURL := url.URL{
Path: newPath,
RawQuery: r.URL.RawQuery,
Fragment: r.URL.Fragment,
}
http.Redirect(w, r, newURL.String(), statusCode)
}
func (t *TreeMux) lookup(w http.ResponseWriter, r *http.Request) (result LookupResult, found bool) {
result.StatusCode = http.StatusNotFound
path := r.RequestURI
unescapedPath := r.URL.Path
pathLen := len(path)
if pathLen > 0 && t.PathSource == RequestURI {
rawQueryLen := len(r.URL.RawQuery)
if rawQueryLen != 0 || path[pathLen-1] == '?' {
// Remove any query string and the ?.
path = path[:pathLen-rawQueryLen-1]
pathLen = len(path)
}
} else {
// In testing with http.NewRequest,
// RequestURI is not set so just grab URL.Path instead.
path = r.URL.Path
pathLen = len(path)
}
trailingSlash := path[pathLen-1] == '/' && pathLen > 1
if trailingSlash && t.RedirectTrailingSlash {
path = path[:pathLen-1]
unescapedPath = unescapedPath[:len(unescapedPath)-1]
}
n, handler, params := t.root.search(r.Method, path[1:])
if n == nil {
if t.RedirectCleanPath {
// Path was not found. Try cleaning it up and search again.
// TODO Test this
cleanPath := Clean(unescapedPath)
n, handler, params = t.root.search(r.Method, cleanPath[1:])
if n == nil {
// Still nothing found.
return
}
if statusCode, ok := t.redirectStatusCode(r.Method); ok {
// Redirect to the actual path
return LookupResult{statusCode, redirectHandler(cleanPath, statusCode), nil, nil}, true
}
} else {
// Not found.
return
}
}
if handler == nil {
if r.Method == "OPTIONS" && t.OptionsHandler != nil {
handler = t.OptionsHandler
}
if handler == nil {
result.leafHandler = n.leafHandler
result.StatusCode = http.StatusMethodNotAllowed
return
}
}
if !n.isCatchAll || t.RemoveCatchAllTrailingSlash {
if trailingSlash != n.addSlash && t.RedirectTrailingSlash {
if statusCode, ok := t.redirectStatusCode(r.Method); ok {
var h HandlerFunc
if n.addSlash {
// Need to add a slash.
h = redirectHandler(unescapedPath+"/", statusCode)
} else if path != "/" {
// We need to remove the slash. This was already done at the
// beginning of the function.
h = redirectHandler(unescapedPath, statusCode)
}
if h != nil {
return LookupResult{statusCode, h, nil, nil}, true
}
}
}
}
var paramMap map[string]string
if len(params) != 0 {
if len(params) != len(n.leafWildcardNames) {
// Need better behavior here. Should this be a panic?
panic(fmt.Sprintf("httptreemux parameter list length mismatch: %v, %v",
params, n.leafWildcardNames))
}
paramMap = make(map[string]string)
numParams := len(params)
for index := 0; index < numParams; index++ {
paramMap[n.leafWildcardNames[numParams-index-1]] = params[index]
}
}
return LookupResult{http.StatusOK, handler, paramMap, nil}, true
}
// Lookup performs a lookup without actually serving the request or mutating the request or response.
// The return values are a LookupResult and a boolean. The boolean will be true when a handler
// was found or the lookup resulted in a redirect which will point to a real handler. It is false
// for requests which would result in a `StatusNotFound` or `StatusMethodNotAllowed`.
//
// Regardless of the returned boolean's value, the LookupResult may be passed to ServeLookupResult
// to be served appropriately.
func (t *TreeMux) Lookup(w http.ResponseWriter, r *http.Request) (LookupResult, bool) {
if t.SafeAddRoutesWhileRunning {
// In concurrency safe mode, we acquire a read lock on the mutex for any access.
// This is optional to avoid potential performance loss in high-usage scenarios.
t.mutex.RLock()
}
result, found := t.lookup(w, r)
if t.SafeAddRoutesWhileRunning {
t.mutex.RUnlock()
}
return result, found
}
// ServeLookupResult serves a request, given a lookup result from the Lookup function.
func (t *TreeMux) ServeLookupResult(w http.ResponseWriter, r *http.Request, lr LookupResult) {
if lr.handler == nil {
if lr.StatusCode == http.StatusMethodNotAllowed && lr.leafHandler != nil {
if t.SafeAddRoutesWhileRunning {
t.mutex.RLock()
}
t.MethodNotAllowedHandler(w, r, lr.leafHandler)
if t.SafeAddRoutesWhileRunning {
t.mutex.RUnlock()
}
} else {
t.NotFoundHandler(w, r)
}
} else {
r = t.setDefaultRequestContext(r)
lr.handler(w, r, lr.params)
}
}
func (t *TreeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if t.PanicHandler != nil {
defer t.serveHTTPPanic(w, r)
}
if t.SafeAddRoutesWhileRunning {
// In concurrency safe mode, we acquire a read lock on the mutex for any access.
// This is optional to avoid potential performance loss in high-usage scenarios.
t.mutex.RLock()
}
result, _ := t.lookup(w, r)
if t.SafeAddRoutesWhileRunning {
t.mutex.RUnlock()
}
t.ServeLookupResult(w, r, result)
}
// MethodNotAllowedHandler is the default handler for TreeMux.MethodNotAllowedHandler,
// which is called for patterns that match, but do not have a handler installed for the
// requested method. It simply writes the status code http.StatusMethodNotAllowed and fills
// in the `Allow` header value appropriately.
func MethodNotAllowedHandler(w http.ResponseWriter, r *http.Request,
methods map[string]HandlerFunc) {
for m := range methods {
w.Header().Add("Allow", m)
}
w.WriteHeader(http.StatusMethodNotAllowed)
}
func New() *TreeMux {
tm := &TreeMux{
root: &node{path: "/"},
NotFoundHandler: http.NotFound,
MethodNotAllowedHandler: MethodNotAllowedHandler,
HeadCanUseGet: true,
RedirectTrailingSlash: true,
RedirectCleanPath: true,
RedirectBehavior: Redirect301,
RedirectMethodBehavior: make(map[string]RedirectBehavior),
PathSource: RequestURI,
EscapeAddedRoutes: false,
}
tm.Group.mux = tm
return tm
}

View file

@ -1,340 +0,0 @@
package httptreemux
import (
"fmt"
"strings"
)
type node struct {
path string
priority int
// The list of static children to check.
staticIndices []byte
staticChild []*node
// If none of the above match, check the wildcard children
wildcardChild *node
// If none of the above match, then we use the catch-all, if applicable.
catchAllChild *node
// Data for the node is below.
addSlash bool
isCatchAll bool
// If true, the head handler was set implicitly, so let it also be set explicitly.
implicitHead bool
// If this node is the end of the URL, then call the handler, if applicable.
leafHandler map[string]HandlerFunc
// The names of the parameters to apply.
leafWildcardNames []string
}
func (n *node) sortStaticChild(i int) {
for i > 0 && n.staticChild[i].priority > n.staticChild[i-1].priority {
n.staticChild[i], n.staticChild[i-1] = n.staticChild[i-1], n.staticChild[i]
n.staticIndices[i], n.staticIndices[i-1] = n.staticIndices[i-1], n.staticIndices[i]
i -= 1
}
}
func (n *node) setHandler(verb string, handler HandlerFunc, implicitHead bool) {
if n.leafHandler == nil {
n.leafHandler = make(map[string]HandlerFunc)
}
_, ok := n.leafHandler[verb]
if ok && (verb != "HEAD" || !n.implicitHead) {
panic(fmt.Sprintf("%s already handles %s", n.path, verb))
}
n.leafHandler[verb] = handler
if verb == "HEAD" {
n.implicitHead = implicitHead
}
}
func (n *node) addPath(path string, wildcards []string, inStaticToken bool) *node {
leaf := len(path) == 0
if leaf {
if wildcards != nil {
// Make sure the current wildcards are the same as the old ones.
// If not then we have an ambiguous path.
if n.leafWildcardNames != nil {
if len(n.leafWildcardNames) != len(wildcards) {
// This should never happen.
panic("Reached leaf node with differing wildcard array length. Please report this as a bug.")
}
for i := 0; i < len(wildcards); i++ {
if n.leafWildcardNames[i] != wildcards[i] {
panic(fmt.Sprintf("Wildcards %v are ambiguous with wildcards %v",
n.leafWildcardNames, wildcards))
}
}
} else {
// No wildcards yet, so just add the existing set.
n.leafWildcardNames = wildcards
}
}
return n
}
c := path[0]
nextSlash := strings.Index(path, "/")
var thisToken string
var tokenEnd int
if c == '/' {
// Done processing the previous token, so reset inStaticToken to false.
thisToken = "/"
tokenEnd = 1
} else if nextSlash == -1 {
thisToken = path
tokenEnd = len(path)
} else {
thisToken = path[0:nextSlash]
tokenEnd = nextSlash
}
remainingPath := path[tokenEnd:]
if c == '*' && !inStaticToken {
// Token starts with a *, so it's a catch-all
thisToken = thisToken[1:]
if n.catchAllChild == nil {
n.catchAllChild = &node{path: thisToken, isCatchAll: true}
}
if path[1:] != n.catchAllChild.path {
panic(fmt.Sprintf("Catch-all name in %s doesn't match %s. You probably tried to define overlapping catchalls",
path, n.catchAllChild.path))
}
if nextSlash != -1 {
panic("/ after catch-all found in " + path)
}
if wildcards == nil {
wildcards = []string{thisToken}
} else {
wildcards = append(wildcards, thisToken)
}
n.catchAllChild.leafWildcardNames = wildcards
return n.catchAllChild
} else if c == ':' && !inStaticToken {
// Token starts with a :
thisToken = thisToken[1:]
if wildcards == nil {
wildcards = []string{thisToken}
} else {
wildcards = append(wildcards, thisToken)
}
if n.wildcardChild == nil {
n.wildcardChild = &node{path: "wildcard"}
}
return n.wildcardChild.addPath(remainingPath, wildcards, false)
} else {
// if strings.ContainsAny(thisToken, ":*") {
// panic("* or : in middle of path component " + path)
// }
unescaped := false
if len(thisToken) >= 2 && !inStaticToken {
if thisToken[0] == '\\' && (thisToken[1] == '*' || thisToken[1] == ':' || thisToken[1] == '\\') {
// The token starts with a character escaped by a backslash. Drop the backslash.
c = thisToken[1]
thisToken = thisToken[1:]
unescaped = true
}
}
// Set inStaticToken to ensure that the rest of this token is not mistaken
// for a wildcard if a prefix split occurs at a '*' or ':'.
inStaticToken = (c != '/')
// Do we have an existing node that starts with the same letter?
for i, index := range n.staticIndices {
if c == index {
// Yes. Split it based on the common prefix of the existing
// node and the new one.
child, prefixSplit := n.splitCommonPrefix(i, thisToken)
child.priority++
n.sortStaticChild(i)
if unescaped {
// Account for the removed backslash.
prefixSplit++
}
return child.addPath(path[prefixSplit:], wildcards, inStaticToken)
}
}
// No existing node starting with this letter, so create it.
child := &node{path: thisToken}
if n.staticIndices == nil {
n.staticIndices = []byte{c}
n.staticChild = []*node{child}
} else {
n.staticIndices = append(n.staticIndices, c)
n.staticChild = append(n.staticChild, child)
}
return child.addPath(remainingPath, wildcards, inStaticToken)
}
}
func (n *node) splitCommonPrefix(existingNodeIndex int, path string) (*node, int) {
childNode := n.staticChild[existingNodeIndex]
if strings.HasPrefix(path, childNode.path) {
// No split needs to be done. Rather, the new path shares the entire
// prefix with the existing node, so the new node is just a child of
// the existing one. Or the new path is the same as the existing path,
// which means that we just move on to the next token. Either way,
// this return accomplishes that
return childNode, len(childNode.path)
}
var i int
// Find the length of the common prefix of the child node and the new path.
for i = range childNode.path {
if i == len(path) {
break
}
if path[i] != childNode.path[i] {
break
}
}
commonPrefix := path[0:i]
childNode.path = childNode.path[i:]
// Create a new intermediary node in the place of the existing node, with
// the existing node as a child.
newNode := &node{
path: commonPrefix,
priority: childNode.priority,
// Index is the first letter of the non-common part of the path.
staticIndices: []byte{childNode.path[0]},
staticChild: []*node{childNode},
}
n.staticChild[existingNodeIndex] = newNode
return newNode, i
}
func (n *node) search(method, path string) (found *node, handler HandlerFunc, params []string) {
// if test != nil {
// test.Logf("Searching for %s in %s", path, n.dumpTree("", ""))
// }
pathLen := len(path)
if pathLen == 0 {
if len(n.leafHandler) == 0 {
return nil, nil, nil
} else {
return n, n.leafHandler[method], nil
}
}
// First see if this matches a static token.
firstChar := path[0]
for i, staticIndex := range n.staticIndices {
if staticIndex == firstChar {
child := n.staticChild[i]
childPathLen := len(child.path)
if pathLen >= childPathLen && child.path == path[:childPathLen] {
nextPath := path[childPathLen:]
found, handler, params = child.search(method, nextPath)
}
break
}
}
// If we found a node and it had a valid handler, then return here. Otherwise
// let's remember that we found this one, but look for a better match.
if handler != nil {
return
}
if n.wildcardChild != nil {
// Didn't find a static token, so check for a wildcard.
nextSlash := strings.IndexByte(path, '/')
if nextSlash < 0 {
nextSlash = pathLen
}
thisToken := path[0:nextSlash]
nextToken := path[nextSlash:]
if len(thisToken) > 0 { // Don't match on empty tokens.
wcNode, wcHandler, wcParams := n.wildcardChild.search(method, nextToken)
if wcHandler != nil || (found == nil && wcNode != nil) {
unescaped, err := unescape(thisToken)
if err != nil {
unescaped = thisToken
}
if wcParams == nil {
wcParams = []string{unescaped}
} else {
wcParams = append(wcParams, unescaped)
}
if wcHandler != nil {
return wcNode, wcHandler, wcParams
}
// Didn't actually find a handler here, so remember that we
// found a node but also see if we can fall through to the
// catchall.
found = wcNode
handler = wcHandler
params = wcParams
}
}
}
catchAllChild := n.catchAllChild
if catchAllChild != nil {
// Hit the catchall, so just assign the whole remaining path if it
// has a matching handler.
handler = catchAllChild.leafHandler[method]
// Found a handler, or we found a catchall node without a handler.
// Either way, return it since there's nothing left to check after this.
if handler != nil || found == nil {
unescaped, err := unescape(path)
if err != nil {
unescaped = path
}
return catchAllChild, handler, []string{unescaped}
}
}
return found, handler, params
}
func (n *node) dumpTree(prefix, nodeType string) string {
line := fmt.Sprintf("%s %02d %s%s [%d] %v wildcards %v\n", prefix, n.priority, nodeType, n.path,
len(n.staticChild), n.leafHandler, n.leafWildcardNames)
prefix += " "
for _, node := range n.staticChild {
line += node.dumpTree(prefix, "")
}
if n.wildcardChild != nil {
line += n.wildcardChild.dumpTree(prefix, ":")
}
if n.catchAllChild != nil {
line += n.catchAllChild.dumpTree(prefix, "*")
}
return line
}

View file

@ -1,86 +0,0 @@
// +build !go1.7
package httptreemux
import (
"net/http"
"sync"
)
type TreeMux struct {
root *node
mutex sync.RWMutex
Group
// The default PanicHandler just returns a 500 code.
PanicHandler PanicHandler
// The default NotFoundHandler is http.NotFound.
NotFoundHandler func(w http.ResponseWriter, r *http.Request)
// Any OPTIONS request that matches a path without its own OPTIONS handler will use this handler,
// if set, instead of calling MethodNotAllowedHandler.
OptionsHandler HandlerFunc
// MethodNotAllowedHandler is called when a pattern matches, but that
// pattern does not have a handler for the requested method. The default
// handler just writes the status code http.StatusMethodNotAllowed and adds
// the required Allowed header.
// The methods parameter contains the map of each method to the corresponding
// handler function.
MethodNotAllowedHandler func(w http.ResponseWriter, r *http.Request,
methods map[string]HandlerFunc)
// HeadCanUseGet allows the router to use the GET handler to respond to
// HEAD requests if no explicit HEAD handler has been added for the
// matching pattern. This is true by default.
HeadCanUseGet bool
// RedirectCleanPath allows the router to try clean the current request path,
// if no handler is registered for it, using CleanPath from github.com/dimfeld/httppath.
// This is true by default.
RedirectCleanPath bool
// RedirectTrailingSlash enables automatic redirection in case router doesn't find a matching route
// for the current request path but a handler for the path with or without the trailing
// slash exists. This is true by default.
RedirectTrailingSlash bool
// RemoveCatchAllTrailingSlash removes the trailing slash when a catch-all pattern
// is matched, if set to true. By default, catch-all paths are never redirected.
RemoveCatchAllTrailingSlash bool
// RedirectBehavior sets the default redirect behavior when RedirectTrailingSlash or
// RedirectCleanPath are true. The default value is Redirect301.
RedirectBehavior RedirectBehavior
// RedirectMethodBehavior overrides the default behavior for a particular HTTP method.
// The key is the method name, and the value is the behavior to use for that method.
RedirectMethodBehavior map[string]RedirectBehavior
// PathSource determines from where the router gets its path to search.
// By default it pulls the data from the RequestURI member, but this can
// be overridden to use URL.Path instead.
//
// There is a small tradeoff here. Using RequestURI allows the router to handle
// encoded slashes (i.e. %2f) in the URL properly, while URL.Path provides
// better compatibility with some utility functions in the http
// library that modify the Request before passing it to the router.
PathSource PathSource
// EscapeAddedRoutes controls URI escaping behavior when adding a route to the tree.
// If set to true, the router will add both the route as originally passed, and
// a version passed through URL.EscapedPath. This behavior is disabled by default.
EscapeAddedRoutes bool
// SafeAddRoutesWhileRunning tells the router to protect all accesses to the tree with an RWMutex. This is only needed
// if you are going to add routes after the router has already begun serving requests. There is a potential
// performance penalty at high load.
SafeAddRoutesWhileRunning bool
}
func (t *TreeMux) setDefaultRequestContext(r *http.Request) *http.Request {
// Nothing to do on Go 1.6 and before
return r
}

View file

@ -1,149 +0,0 @@
// +build go1.7
package httptreemux
import (
"context"
"net/http"
"sync"
)
type TreeMux struct {
root *node
mutex sync.RWMutex
Group
// The default PanicHandler just returns a 500 code.
PanicHandler PanicHandler
// The default NotFoundHandler is http.NotFound.
NotFoundHandler func(w http.ResponseWriter, r *http.Request)
// Any OPTIONS request that matches a path without its own OPTIONS handler will use this handler,
// if set, instead of calling MethodNotAllowedHandler.
OptionsHandler HandlerFunc
// MethodNotAllowedHandler is called when a pattern matches, but that
// pattern does not have a handler for the requested method. The default
// handler just writes the status code http.StatusMethodNotAllowed and adds
// the required Allowed header.
// The methods parameter contains the map of each method to the corresponding
// handler function.
MethodNotAllowedHandler func(w http.ResponseWriter, r *http.Request,
methods map[string]HandlerFunc)
// HeadCanUseGet allows the router to use the GET handler to respond to
// HEAD requests if no explicit HEAD handler has been added for the
// matching pattern. This is true by default.
HeadCanUseGet bool
// RedirectCleanPath allows the router to try clean the current request path,
// if no handler is registered for it, using CleanPath from github.com/dimfeld/httppath.
// This is true by default.
RedirectCleanPath bool
// RedirectTrailingSlash enables automatic redirection in case router doesn't find a matching route
// for the current request path but a handler for the path with or without the trailing
// slash exists. This is true by default.
RedirectTrailingSlash bool
// RemoveCatchAllTrailingSlash removes the trailing slash when a catch-all pattern
// is matched, if set to true. By default, catch-all paths are never redirected.
RemoveCatchAllTrailingSlash bool
// RedirectBehavior sets the default redirect behavior when RedirectTrailingSlash or
// RedirectCleanPath are true. The default value is Redirect301.
RedirectBehavior RedirectBehavior
// RedirectMethodBehavior overrides the default behavior for a particular HTTP method.
// The key is the method name, and the value is the behavior to use for that method.
RedirectMethodBehavior map[string]RedirectBehavior
// PathSource determines from where the router gets its path to search.
// By default it pulls the data from the RequestURI member, but this can
// be overridden to use URL.Path instead.
//
// There is a small tradeoff here. Using RequestURI allows the router to handle
// encoded slashes (i.e. %2f) in the URL properly, while URL.Path provides
// better compatibility with some utility functions in the http
// library that modify the Request before passing it to the router.
PathSource PathSource
// EscapeAddedRoutes controls URI escaping behavior when adding a route to the tree.
// If set to true, the router will add both the route as originally passed, and
// a version passed through URL.EscapedPath. This behavior is disabled by default.
EscapeAddedRoutes bool
// If present, override the default context with this one.
DefaultContext context.Context
// SafeAddRoutesWhileRunning tells the router to protect all accesses to the tree with an RWMutex. This is only needed
// if you are going to add routes after the router has already begun serving requests. There is a potential
// performance penalty at high load.
SafeAddRoutesWhileRunning bool
}
func (t *TreeMux) setDefaultRequestContext(r *http.Request) *http.Request {
if t.DefaultContext != nil {
r = r.WithContext(t.DefaultContext)
}
return r
}
type ContextMux struct {
*TreeMux
*ContextGroup
}
// NewContextMux returns a TreeMux preconfigured to work with standard http
// Handler functions and context objects.
func NewContextMux() *ContextMux {
mux := New()
cg := mux.UsingContext()
return &ContextMux{
TreeMux: mux,
ContextGroup: cg,
}
}
func (cm *ContextMux) NewGroup(path string) *ContextGroup {
return cm.ContextGroup.NewGroup(path)
}
// GET is convenience method for handling GET requests on a context group.
func (cm *ContextMux) GET(path string, handler http.HandlerFunc) {
cm.ContextGroup.Handle("GET", path, handler)
}
// POST is convenience method for handling POST requests on a context group.
func (cm *ContextMux) POST(path string, handler http.HandlerFunc) {
cm.ContextGroup.Handle("POST", path, handler)
}
// PUT is convenience method for handling PUT requests on a context group.
func (cm *ContextMux) PUT(path string, handler http.HandlerFunc) {
cm.ContextGroup.Handle("PUT", path, handler)
}
// DELETE is convenience method for handling DELETE requests on a context group.
func (cm *ContextMux) DELETE(path string, handler http.HandlerFunc) {
cm.ContextGroup.Handle("DELETE", path, handler)
}
// PATCH is convenience method for handling PATCH requests on a context group.
func (cm *ContextMux) PATCH(path string, handler http.HandlerFunc) {
cm.ContextGroup.Handle("PATCH", path, handler)
}
// HEAD is convenience method for handling HEAD requests on a context group.
func (cm *ContextMux) HEAD(path string, handler http.HandlerFunc) {
cm.ContextGroup.Handle("HEAD", path, handler)
}
// OPTIONS is convenience method for handling OPTIONS requests on a context group.
func (cm *ContextMux) OPTIONS(path string, handler http.HandlerFunc) {
cm.ContextGroup.Handle("OPTIONS", path, handler)
}

View file

@ -1,9 +0,0 @@
// +build !go1.8
package httptreemux
import "net/url"
func unescape(path string) (string, error) {
return url.QueryUnescape(path)
}

View file

@ -1,9 +0,0 @@
// +build go1.8
package httptreemux
import "net/url"
func unescape(path string) (string, error) {
return url.PathUnescape(path)
}

3
vendor/modules.txt vendored
View file

@ -31,9 +31,6 @@ github.com/containerd/containerd/platforms
github.com/cpuguy83/go-md2man/v2/md2man
# github.com/davecgh/go-spew v1.1.1
github.com/davecgh/go-spew/spew
# github.com/dimfeld/httptreemux v5.0.1+incompatible
## explicit
github.com/dimfeld/httptreemux
# github.com/docker/cli v0.0.0-20200303215952-eb310fca4956
## explicit
github.com/docker/cli/cli/config/configfile

0
web/dist/.gitkeep vendored Normal file
View file

3
web/dist/dist.go vendored
View file

@ -1,3 +0,0 @@
package dist
//go:generate togo http -package dist -output dist_gen.go

11108
web/dist/dist_gen.go vendored

File diff suppressed because one or more lines are too long

44
web/web.go Normal file
View file

@ -0,0 +1,44 @@
package web
import (
"embed"
"io/fs"
"io/ioutil"
"net/http"
)
//go:embed dist/*
var webFiles embed.FS
func HttpFS() http.FileSystem {
httpFS, err := fs.Sub(webFiles, "dist")
if err != nil {
panic(err)
}
return http.FS(httpFS)
}
func Lookup(path string) (buf []byte, err error) {
file, err := HttpFS().Open(path)
if err != nil {
return nil, err
}
defer file.Close()
buf, err = ioutil.ReadAll(file)
if err != nil {
return nil, err
}
return buf, nil
}
func MustLookup(path string) (buf []byte) {
buf, err := Lookup(path)
if err != nil {
panic(err)
}
return buf
}

View file

@ -32,7 +32,7 @@ module.exports = {
// where to dump the output of a production build
output: {
publicPath: "/",
path: path.join(__dirname, "dist/files"),
path: path.join(__dirname, "dist"),
filename: "static/bundle.[chunkhash].js"
},