Updated dependencies

This commit is contained in:
claudemiro
2016-08-09 23:22:23 -03:00
parent 2eac54a0d3
commit f0b3aadf74
40 changed files with 1712 additions and 632 deletions
-3
View File
@@ -1,3 +0,0 @@
[submodule "vendor/github.com/pusher/pusher-http-go"]
path = vendor/github.com/pusher/pusher-http-go
url = git://github.com/pusher/pusher-http-go.git
+1 -1
View File
@@ -11,7 +11,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.3.4/mocha.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.4.1/chai.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.4.1/chai.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/3.0.0/pusher.js"></script> <script src="//js.pusher.com/3.2/pusher.min.js"></script>
<script>mocha.setup('bdd')</script> <script>mocha.setup('bdd')</script>
<script src="test.pusher.js"></script> <script src="test.pusher.js"></script>
+5
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"github.com/pusher/pusher-http-go" "github.com/pusher/pusher-http-go"
@@ -20,6 +21,7 @@ func init() {
} }
func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) { func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) {
log.Println("Presence Request")
presenceData := pusher.MemberData{ presenceData := pusher.MemberData{
UserId: "1", UserId: "1",
UserInfo: map[string]string{}, UserInfo: map[string]string{},
@@ -39,6 +41,9 @@ func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
params, _ := ioutil.ReadAll(req.Body) params, _ := ioutil.ReadAll(req.Body)
response, err := client.AuthenticatePrivateChannel(params) response, err := client.AuthenticatePrivateChannel(params)
log.Printf("Private Request %s", params)
log.Printf("Auth %s", response)
if err != nil { if err != nil {
panic(err) panic(err)
} }
Generated
+6 -6
View File
@@ -1,14 +1,14 @@
hash: bfb508bdf4f85c71c7eef4bb9431148ee373dfaf31d001d050d933f80ac4288e hash: bfb508bdf4f85c71c7eef4bb9431148ee373dfaf31d001d050d933f80ac4288e
updated: 2016-02-21T20:13:57.03748112-03:00 updated: 2016-08-09T22:49:29.988191345-03:00
imports: imports:
- name: github.com/golang/glog - name: github.com/golang/glog
version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998 version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998
- name: github.com/gorilla/context - name: github.com/gorilla/context
version: 1c83b3eabd45b6d76072b66b746c20815fb2872d version: aed02d124ae4a0e94fea4541c8effd05bf0c8296
- name: github.com/gorilla/mux - name: github.com/gorilla/mux
version: 26a6070f849969ba72b72256e9f14cf519751690 version: 780415097119f6f61c55475fe59b66f3c3e9ea53
- name: github.com/gorilla/websocket - name: github.com/gorilla/websocket
version: 5c91b59efa232fa9a87b705d54101832c498a172 version: a69d25be2fe2923a97c2af6849b2f52426f68fc0
- name: github.com/pusher/pusher-http-go - name: github.com/pusher/pusher-http-go
version: 8d4ffe157699620440932e4d03253a22533f2e43 version: 2bba5f217f6f0f4f0c0a9bb11b945b206b32bec5
devImports: [] testImports: []
+15 -5
View File
@@ -1,8 +1,18 @@
language: go language: go
sudo: false sudo: false
go: matrix:
- 1.3 include:
- 1.4 - go: 1.3
- 1.5 - go: 1.4
- tip - go: 1.5
- go: 1.6
- go: tip
allow_failures:
- go: tip
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- go vet $(go list ./... | grep -v /vendor/)
- go test -v -race ./...
+1 -1
View File
@@ -69,7 +69,7 @@ func TestContext(t *testing.T) {
// GetAllOk() for empty request // GetAllOk() for empty request
values, ok = GetAllOk(emptyR) values, ok = GetAllOk(emptyR)
assertEqual(value, nil) assertEqual(len(values), 0)
assertEqual(ok, false) assertEqual(ok, false)
// Delete() // Delete()
+13 -7
View File
@@ -1,14 +1,20 @@
language: go language: go
sudo: false sudo: false
go:
- 1.3 matrix:
- 1.4 include:
- 1.5 - go: 1.2
- tip - go: 1.3
- go: 1.4
- go: 1.5
- go: 1.6
- go: tip
install: install:
- go get golang.org/x/tools/cmd/vet - # Skip
script: script:
- go get -t -v ./... - go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d -s .) - diff -u <(echo -n) <(gofmt -d .)
- go tool vet . - go tool vet .
- go test -v -race ./... - go test -v -race ./...
+65 -6
View File
@@ -1,17 +1,43 @@
mux gorilla/mux
=== ===
[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) [![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux)
[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) [![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux)
Package `gorilla/mux` implements a request router and dispatcher. ![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png)
http://www.gorillatoolkit.org/pkg/mux
Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to
their respective handler.
The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are:
* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`.
* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. * Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers.
* URL hosts and paths can have variables with an optional regular expression. * URL hosts and paths can have variables with an optional regular expression.
* Registered URLs can be built, or "reversed", which helps maintaining references to resources. * Registered URLs can be built, or "reversed", which helps maintaining references to resources.
* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. * Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching.
* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`.
---
* [Install](#install)
* [Examples](#examples)
* [Matching Routes](#matching-routes)
* [Static Files](#static-files)
* [Registered URLs](#registered-urls)
* [Full Example](#full-example)
---
## Install
With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain:
```sh
go get -u github.com/gorilla/mux
```
## Examples
Let's start registering a couple of URL paths and handlers: Let's start registering a couple of URL paths and handlers:
@@ -45,6 +71,8 @@ category := vars["category"]
And this is all you need to know about the basic usage. More advanced options are explained below. And this is all you need to know about the basic usage. More advanced options are explained below.
### Matching Routes
Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables:
```go ```go
@@ -116,7 +144,7 @@ Then register routes in the subrouter:
```go ```go
s.HandleFunc("/products/", ProductsHandler) s.HandleFunc("/products/", ProductsHandler)
s.HandleFunc("/products/{key}", ProductHandler) s.HandleFunc("/products/{key}", ProductHandler)
s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
``` ```
The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route.
@@ -136,6 +164,37 @@ s.HandleFunc("/{key}/", ProductHandler)
s.HandleFunc("/{key}/details", ProductDetailsHandler) s.HandleFunc("/{key}/details", ProductDetailsHandler)
``` ```
### Static Files
Note that the path provided to `PathPrefix()` represents a "wildcard": calling
`PathPrefix("/static/").Handler(...)` means that the handler will be passed any
request that matches "/static/*". This makes it easy to serve static files with mux:
```go
func main() {
var dir string
flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir")
flag.Parse()
r := mux.NewRouter()
// This will serve files under http://localhost:8000/static/<filename>
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir))))
srv := &http.Server{
Handler: r,
Addr: "127.0.0.1:8000",
// Good practice: enforce timeouts for servers you create!
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
```
### Registered URLs
Now let's see how to build registered URLs. Now let's see how to build registered URLs.
Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example:
@@ -217,7 +276,7 @@ package main
import ( import (
"net/http" "net/http"
"log"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@@ -231,7 +290,7 @@ func main() {
r.HandleFunc("/", YourHandler) r.HandleFunc("/", YourHandler)
// Bind to a port and pass our router in // Bind to a port and pass our router in
http.ListenAndServe(":8000", r) log.Fatal(http.ListenAndServe(":8000", r))
} }
``` ```
+28
View File
@@ -6,6 +6,7 @@ package mux
import ( import (
"net/http" "net/http"
"net/http/httptest"
"testing" "testing"
) )
@@ -19,3 +20,30 @@ func BenchmarkMux(b *testing.B) {
router.ServeHTTP(nil, request) router.ServeHTTP(nil, request)
} }
} }
func BenchmarkMuxAlternativeInRegexp(b *testing.B) {
router := new(Router)
handler := func(w http.ResponseWriter, r *http.Request) {}
router.HandleFunc("/v1/{v1:(a|b)}", handler)
requestA, _ := http.NewRequest("GET", "/v1/a", nil)
requestB, _ := http.NewRequest("GET", "/v1/b", nil)
for i := 0; i < b.N; i++ {
router.ServeHTTP(nil, requestA)
router.ServeHTTP(nil, requestB)
}
}
func BenchmarkManyPathVariables(b *testing.B) {
router := new(Router)
handler := func(w http.ResponseWriter, r *http.Request) {}
router.HandleFunc("/v1/{v1}/{v2}/{v3}/{v4}/{v5}", handler)
matchingRequest, _ := http.NewRequest("GET", "/v1/1/2/3/4/5", nil)
notMatchingRequest, _ := http.NewRequest("GET", "/v1/1/2/3/4", nil)
recorder := httptest.NewRecorder()
for i := 0; i < b.N; i++ {
router.ServeHTTP(nil, matchingRequest)
router.ServeHTTP(recorder, notMatchingRequest)
}
}
+26
View File
@@ -0,0 +1,26 @@
// +build !go1.7
package mux
import (
"net/http"
"github.com/gorilla/context"
)
func contextGet(r *http.Request, key interface{}) interface{} {
return context.Get(r, key)
}
func contextSet(r *http.Request, key, val interface{}) *http.Request {
if val == nil {
return r
}
context.Set(r, key, val)
return r
}
func contextClear(r *http.Request) {
context.Clear(r)
}
+40
View File
@@ -0,0 +1,40 @@
// +build !go1.7
package mux
import (
"net/http"
"testing"
"github.com/gorilla/context"
)
// Tests that the context is cleared or not cleared properly depending on
// the configuration of the router
func TestKeepContext(t *testing.T) {
func1 := func(w http.ResponseWriter, r *http.Request) {}
r := NewRouter()
r.HandleFunc("/", func1).Name("func1")
req, _ := http.NewRequest("GET", "http://localhost/", nil)
context.Set(req, "t", 1)
res := new(http.ResponseWriter)
r.ServeHTTP(*res, req)
if _, ok := context.GetOk(req, "t"); ok {
t.Error("Context should have been cleared at end of request")
}
r.KeepContext = true
req, _ = http.NewRequest("GET", "http://localhost/", nil)
context.Set(req, "t", 1)
r.ServeHTTP(*res, req)
if _, ok := context.GetOk(req, "t"); !ok {
t.Error("Context should NOT have been cleared at end of request")
}
}
+24
View File
@@ -0,0 +1,24 @@
// +build go1.7
package mux
import (
"context"
"net/http"
)
func contextGet(r *http.Request, key interface{}) interface{} {
return r.Context().Value(key)
}
func contextSet(r *http.Request, key, val interface{}) *http.Request {
if val == nil {
return r
}
return r.WithContext(context.WithValue(r.Context(), key, val))
}
func contextClear(r *http.Request) {
return
}
+32
View File
@@ -0,0 +1,32 @@
// +build go1.7
package mux
import (
"context"
"net/http"
"testing"
"time"
)
func TestNativeContextMiddleware(t *testing.T) {
withTimeout := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
h.ServeHTTP(w, r.WithContext(ctx))
})
}
r := NewRouter()
r.Handle("/path/{foo}", withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := Vars(r)
if vars["foo"] != "bar" {
t.Fatal("Expected foo var to be set")
}
})))
rec := NewRecorder()
req := newRequest("GET", "/path/bar")
r.ServeHTTP(rec, req)
}
+26 -1
View File
@@ -3,7 +3,7 @@
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
/* /*
Package gorilla/mux implements a request router and dispatcher. Package mux implements a request router and dispatcher.
The name mux stands for "HTTP request multiplexer". Like the standard The name mux stands for "HTTP request multiplexer". Like the standard
http.ServeMux, mux.Router matches incoming requests against a list of http.ServeMux, mux.Router matches incoming requests against a list of
@@ -136,6 +136,31 @@ the inner routes use it as base for their paths:
// "/products/{key}/details" // "/products/{key}/details"
s.HandleFunc("/{key}/details", ProductDetailsHandler) s.HandleFunc("/{key}/details", ProductDetailsHandler)
Note that the path provided to PathPrefix() represents a "wildcard": calling
PathPrefix("/static/").Handler(...) means that the handler will be passed any
request that matches "/static/*". This makes it easy to serve static files with mux:
func main() {
var dir string
flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir")
flag.Parse()
r := mux.NewRouter()
// This will serve files under http://localhost:8000/static/<filename>
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir))))
srv := &http.Server{
Handler: r,
Addr: "127.0.0.1:8000",
// Good practice: enforce timeouts for servers you create!
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
Now let's see how to build registered URLs. Now let's see how to build registered URLs.
Routes can be named. All routes that define a name can have their URLs built, Routes can be named. All routes that define a name can have their URLs built,
+46 -29
View File
@@ -10,8 +10,6 @@ import (
"net/http" "net/http"
"path" "path"
"regexp" "regexp"
"github.com/gorilla/context"
) )
// NewRouter returns a new router instance. // NewRouter returns a new router instance.
@@ -48,7 +46,11 @@ type Router struct {
namedRoutes map[string]*Route namedRoutes map[string]*Route
// See Router.StrictSlash(). This defines the flag for new routes. // See Router.StrictSlash(). This defines the flag for new routes.
strictSlash bool strictSlash bool
// If true, do not clear the request context after handling the request // See Router.SkipClean(). This defines the flag for new routes.
skipClean bool
// If true, do not clear the request context after handling the request.
// This has no effect when go1.7+ is used, since the context is stored
// on the request itself.
KeepContext bool KeepContext bool
} }
@@ -73,32 +75,34 @@ func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
// When there is a match, the route variables can be retrieved calling // When there is a match, the route variables can be retrieved calling
// mux.Vars(request). // mux.Vars(request).
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Clean path to canonical form and redirect. if !r.skipClean {
if p := cleanPath(req.URL.Path); p != req.URL.Path { // Clean path to canonical form and redirect.
if p := cleanPath(req.URL.Path); p != req.URL.Path {
// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query. // Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
// This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue:
// http://code.google.com/p/go/issues/detail?id=5252 // http://code.google.com/p/go/issues/detail?id=5252
url := *req.URL url := *req.URL
url.Path = p url.Path = p
p = url.String() p = url.String()
w.Header().Set("Location", p) w.Header().Set("Location", p)
w.WriteHeader(http.StatusMovedPermanently) w.WriteHeader(http.StatusMovedPermanently)
return return
}
} }
var match RouteMatch var match RouteMatch
var handler http.Handler var handler http.Handler
if r.Match(req, &match) { if r.Match(req, &match) {
handler = match.Handler handler = match.Handler
setVars(req, match.Vars) req = setVars(req, match.Vars)
setCurrentRoute(req, match.Route) req = setCurrentRoute(req, match.Route)
} }
if handler == nil { if handler == nil {
handler = http.NotFoundHandler() handler = http.NotFoundHandler()
} }
if !r.KeepContext { if !r.KeepContext {
defer context.Clear(req) defer contextClear(req)
} }
handler.ServeHTTP(w, req) handler.ServeHTTP(w, req)
} }
@@ -133,6 +137,19 @@ func (r *Router) StrictSlash(value bool) *Router {
return r return r
} }
// SkipClean defines the path cleaning behaviour for new routes. The initial
// value is false. Users should be careful about which routes are not cleaned
//
// When true, if the route path is "/path//to", it will remain with the double
// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/
//
// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will
// become /fetch/http/xkcd.com/534
func (r *Router) SkipClean(value bool) *Router {
r.skipClean = value
return r
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// parentRoute // parentRoute
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -170,7 +187,7 @@ func (r *Router) buildVars(m map[string]string) map[string]string {
// NewRoute registers an empty route. // NewRoute registers an empty route.
func (r *Router) NewRoute() *Route { func (r *Router) NewRoute() *Route {
route := &Route{parent: r, strictSlash: r.strictSlash} route := &Route{parent: r, strictSlash: r.strictSlash, skipClean: r.skipClean}
r.routes = append(r.routes, route) r.routes = append(r.routes, route)
return route return route
} }
@@ -236,7 +253,7 @@ func (r *Router) Schemes(schemes ...string) *Route {
return r.NewRoute().Schemes(schemes...) return r.NewRoute().Schemes(schemes...)
} }
// BuildVars registers a new route with a custom function for modifying // BuildVarsFunc registers a new route with a custom function for modifying
// route variables before building a URL. // route variables before building a URL.
func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route {
return r.NewRoute().BuildVarsFunc(f) return r.NewRoute().BuildVarsFunc(f)
@@ -268,6 +285,9 @@ func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error {
if err == SkipRouter { if err == SkipRouter {
continue continue
} }
if err != nil {
return err
}
for _, sr := range t.matchers { for _, sr := range t.matchers {
if h, ok := sr.(*Router); ok { if h, ok := sr.(*Router); ok {
err := h.walk(walkFn, ancestors) err := h.walk(walkFn, ancestors)
@@ -308,7 +328,7 @@ const (
// Vars returns the route variables for the current request, if any. // Vars returns the route variables for the current request, if any.
func Vars(r *http.Request) map[string]string { func Vars(r *http.Request) map[string]string {
if rv := context.Get(r, varsKey); rv != nil { if rv := contextGet(r, varsKey); rv != nil {
return rv.(map[string]string) return rv.(map[string]string)
} }
return nil return nil
@@ -320,22 +340,18 @@ func Vars(r *http.Request) map[string]string {
// after the handler returns, unless the KeepContext option is set on the // after the handler returns, unless the KeepContext option is set on the
// Router. // Router.
func CurrentRoute(r *http.Request) *Route { func CurrentRoute(r *http.Request) *Route {
if rv := context.Get(r, routeKey); rv != nil { if rv := contextGet(r, routeKey); rv != nil {
return rv.(*Route) return rv.(*Route)
} }
return nil return nil
} }
func setVars(r *http.Request, val interface{}) { func setVars(r *http.Request, val interface{}) *http.Request {
if val != nil { return contextSet(r, varsKey, val)
context.Set(r, varsKey, val)
}
} }
func setCurrentRoute(r *http.Request, val interface{}) { func setCurrentRoute(r *http.Request, val interface{}) *http.Request {
if val != nil { return contextSet(r, routeKey, val)
context.Set(r, routeKey, val)
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -357,6 +373,7 @@ func cleanPath(p string) string {
if p[len(p)-1] == '/' && np != "/" { if p[len(p)-1] == '/' && np != "/" {
np += "/" np += "/"
} }
return np return np
} }
+420 -302
View File
@@ -5,12 +5,11 @@
package mux package mux
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
"github.com/gorilla/context"
) )
func (r *Route) GoString() string { func (r *Route) GoString() string {
@@ -32,6 +31,8 @@ type routeTest struct {
vars map[string]string // the expected vars of the match vars map[string]string // the expected vars of the match
host string // the expected host of the match host string // the expected host of the match
path string // the expected path of the match path string // the expected path of the match
pathTemplate string // the expected path template to match
hostTemplate string // the expected host template to match
shouldMatch bool // whether the request is expected to match the route at all shouldMatch bool // whether the request is expected to match the route at all
shouldRedirect bool // whether the request should result in a redirect shouldRedirect bool // whether the request should result in a redirect
} }
@@ -113,116 +114,129 @@ func TestHost(t *testing.T) {
shouldMatch: false, shouldMatch: false,
}, },
{ {
title: "Host route with pattern, match", title: "Host route with pattern, match",
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb"}, vars: map[string]string{"v1": "bbb"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "", path: "",
shouldMatch: true, hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`,
shouldMatch: true,
}, },
{ {
title: "Host route with pattern, additional capturing group, match", title: "Host route with pattern, additional capturing group, match",
route: new(Route).Host("aaa.{v1:[a-z]{2}(b|c)}.ccc"), route: new(Route).Host("aaa.{v1:[a-z]{2}(b|c)}.ccc"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb"}, vars: map[string]string{"v1": "bbb"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "", path: "",
shouldMatch: true, hostTemplate: `aaa.{v1:[a-z]{2}(b|c)}.ccc`,
shouldMatch: true,
}, },
{ {
title: "Host route with pattern, wrong host in request URL", title: "Host route with pattern, wrong host in request URL",
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb"}, vars: map[string]string{"v1": "bbb"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "", path: "",
shouldMatch: false, hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`,
shouldMatch: false,
}, },
{ {
title: "Host route with multiple patterns, match", title: "Host route with multiple patterns, match",
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "", path: "",
shouldMatch: true, hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`,
shouldMatch: true,
}, },
{ {
title: "Host route with multiple patterns, wrong host in request URL", title: "Host route with multiple patterns, wrong host in request URL",
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "", path: "",
shouldMatch: false, hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`,
shouldMatch: false,
}, },
{ {
title: "Host route with hyphenated name and pattern, match", title: "Host route with hyphenated name and pattern, match",
route: new(Route).Host("aaa.{v-1:[a-z]{3}}.ccc"), route: new(Route).Host("aaa.{v-1:[a-z]{3}}.ccc"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v-1": "bbb"}, vars: map[string]string{"v-1": "bbb"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "", path: "",
shouldMatch: true, hostTemplate: `aaa.{v-1:[a-z]{3}}.ccc`,
shouldMatch: true,
}, },
{ {
title: "Host route with hyphenated name and pattern, additional capturing group, match", title: "Host route with hyphenated name and pattern, additional capturing group, match",
route: new(Route).Host("aaa.{v-1:[a-z]{2}(b|c)}.ccc"), route: new(Route).Host("aaa.{v-1:[a-z]{2}(b|c)}.ccc"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v-1": "bbb"}, vars: map[string]string{"v-1": "bbb"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "", path: "",
shouldMatch: true, hostTemplate: `aaa.{v-1:[a-z]{2}(b|c)}.ccc`,
shouldMatch: true,
}, },
{ {
title: "Host route with multiple hyphenated names and patterns, match", title: "Host route with multiple hyphenated names and patterns, match",
route: new(Route).Host("{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}"), route: new(Route).Host("{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v-1": "aaa", "v-2": "bbb", "v-3": "ccc"}, vars: map[string]string{"v-1": "aaa", "v-2": "bbb", "v-3": "ccc"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "", path: "",
shouldMatch: true, hostTemplate: `{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}`,
shouldMatch: true,
}, },
{ {
title: "Path route with single pattern with pipe, match", title: "Path route with single pattern with pipe, match",
route: new(Route).Path("/{category:a|b/c}"), route: new(Route).Path("/{category:a|b/c}"),
request: newRequest("GET", "http://localhost/a"), request: newRequest("GET", "http://localhost/a"),
vars: map[string]string{"category": "a"}, vars: map[string]string{"category": "a"},
host: "", host: "",
path: "/a", path: "/a",
shouldMatch: true, pathTemplate: `/{category:a|b/c}`,
shouldMatch: true,
}, },
{ {
title: "Path route with single pattern with pipe, match", title: "Path route with single pattern with pipe, match",
route: new(Route).Path("/{category:a|b/c}"), route: new(Route).Path("/{category:a|b/c}"),
request: newRequest("GET", "http://localhost/b/c"), request: newRequest("GET", "http://localhost/b/c"),
vars: map[string]string{"category": "b/c"}, vars: map[string]string{"category": "b/c"},
host: "", host: "",
path: "/b/c", path: "/b/c",
shouldMatch: true, pathTemplate: `/{category:a|b/c}`,
shouldMatch: true,
}, },
{ {
title: "Path route with multiple patterns with pipe, match", title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/a/product_name/1"), request: newRequest("GET", "http://localhost/a/product_name/1"),
vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, vars: map[string]string{"category": "a", "product": "product_name", "id": "1"},
host: "", host: "",
path: "/a/product_name/1", path: "/a/product_name/1",
shouldMatch: true, pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`,
shouldMatch: true,
}, },
{ {
title: "Path route with multiple patterns with pipe, match", title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/b/c/product_name/1"), request: newRequest("GET", "http://localhost/b/c/product_name/1"),
vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"}, vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"},
host: "", host: "",
path: "/b/c/product_name/1", path: "/b/c/product_name/1",
shouldMatch: true, pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`,
shouldMatch: true,
}, },
} }
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
@@ -247,22 +261,24 @@ func TestPath(t *testing.T) {
shouldMatch: true, shouldMatch: true,
}, },
{ {
title: "Path route, do not match with trailing slash in path", title: "Path route, do not match with trailing slash in path",
route: new(Route).Path("/111/"), route: new(Route).Path("/111/"),
request: newRequest("GET", "http://localhost/111"), request: newRequest("GET", "http://localhost/111"),
vars: map[string]string{}, vars: map[string]string{},
host: "", host: "",
path: "/111", path: "/111",
shouldMatch: false, pathTemplate: `/111/`,
shouldMatch: false,
}, },
{ {
title: "Path route, do not match with trailing slash in request", title: "Path route, do not match with trailing slash in request",
route: new(Route).Path("/111"), route: new(Route).Path("/111"),
request: newRequest("GET", "http://localhost/111/"), request: newRequest("GET", "http://localhost/111/"),
vars: map[string]string{}, vars: map[string]string{},
host: "", host: "",
path: "/111/", path: "/111/",
shouldMatch: false, pathTemplate: `/111`,
shouldMatch: false,
}, },
{ {
title: "Path route, wrong path in request in request URL", title: "Path route, wrong path in request in request URL",
@@ -274,81 +290,100 @@ func TestPath(t *testing.T) {
shouldMatch: false, shouldMatch: false,
}, },
{ {
title: "Path route with pattern, match", title: "Path route with pattern, match",
route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), route: new(Route).Path("/111/{v1:[0-9]{3}}/333"),
request: newRequest("GET", "http://localhost/111/222/333"), request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "222"}, vars: map[string]string{"v1": "222"},
host: "", host: "",
path: "/111/222/333", path: "/111/222/333",
shouldMatch: true, pathTemplate: `/111/{v1:[0-9]{3}}/333`,
shouldMatch: true,
}, },
{ {
title: "Path route with pattern, URL in request does not match", title: "Path route with pattern, URL in request does not match",
route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), route: new(Route).Path("/111/{v1:[0-9]{3}}/333"),
request: newRequest("GET", "http://localhost/111/aaa/333"), request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "222"}, vars: map[string]string{"v1": "222"},
host: "", host: "",
path: "/111/222/333", path: "/111/222/333",
shouldMatch: false, pathTemplate: `/111/{v1:[0-9]{3}}/333`,
shouldMatch: false,
}, },
{ {
title: "Path route with multiple patterns, match", title: "Path route with multiple patterns, match",
route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/222/333"), request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"},
host: "", host: "",
path: "/111/222/333", path: "/111/222/333",
shouldMatch: true, pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`,
shouldMatch: true,
}, },
{ {
title: "Path route with multiple patterns, URL in request does not match", title: "Path route with multiple patterns, URL in request does not match",
route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/aaa/333"), request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"},
host: "", host: "",
path: "/111/222/333", path: "/111/222/333",
shouldMatch: false, pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`,
shouldMatch: false,
}, },
{ {
title: "Path route with multiple patterns with pipe, match", title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|(b/c)}/{product}/{id:[0-9]+}"), route: new(Route).Path("/{category:a|(b/c)}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/a/product_name/1"), request: newRequest("GET", "http://localhost/a/product_name/1"),
vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, vars: map[string]string{"category": "a", "product": "product_name", "id": "1"},
host: "", host: "",
path: "/a/product_name/1", path: "/a/product_name/1",
shouldMatch: true, pathTemplate: `/{category:a|(b/c)}/{product}/{id:[0-9]+}`,
shouldMatch: true,
}, },
{ {
title: "Path route with hyphenated name and pattern, match", title: "Path route with hyphenated name and pattern, match",
route: new(Route).Path("/111/{v-1:[0-9]{3}}/333"), route: new(Route).Path("/111/{v-1:[0-9]{3}}/333"),
request: newRequest("GET", "http://localhost/111/222/333"), request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v-1": "222"}, vars: map[string]string{"v-1": "222"},
host: "", host: "",
path: "/111/222/333", path: "/111/222/333",
shouldMatch: true, pathTemplate: `/111/{v-1:[0-9]{3}}/333`,
shouldMatch: true,
}, },
{ {
title: "Path route with multiple hyphenated names and patterns, match", title: "Path route with multiple hyphenated names and patterns, match",
route: new(Route).Path("/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}"), route: new(Route).Path("/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/222/333"), request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v-1": "111", "v-2": "222", "v-3": "333"}, vars: map[string]string{"v-1": "111", "v-2": "222", "v-3": "333"},
host: "", host: "",
path: "/111/222/333", path: "/111/222/333",
shouldMatch: true, pathTemplate: `/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}`,
shouldMatch: true,
}, },
{ {
title: "Path route with multiple hyphenated names and patterns with pipe, match", title: "Path route with multiple hyphenated names and patterns with pipe, match",
route: new(Route).Path("/{product-category:a|(b/c)}/{product-name}/{product-id:[0-9]+}"), route: new(Route).Path("/{product-category:a|(b/c)}/{product-name}/{product-id:[0-9]+}"),
request: newRequest("GET", "http://localhost/a/product_name/1"), request: newRequest("GET", "http://localhost/a/product_name/1"),
vars: map[string]string{"product-category": "a", "product-name": "product_name", "product-id": "1"}, vars: map[string]string{"product-category": "a", "product-name": "product_name", "product-id": "1"},
host: "", host: "",
path: "/a/product_name/1", path: "/a/product_name/1",
shouldMatch: true, pathTemplate: `/{product-category:a|(b/c)}/{product-name}/{product-id:[0-9]+}`,
shouldMatch: true,
},
{
title: "Path route with multiple hyphenated names and patterns with pipe and case insensitive, match",
route: new(Route).Path("/{type:(?i:daily|mini|variety)}-{date:\\d{4,4}-\\d{2,2}-\\d{2,2}}"),
request: newRequest("GET", "http://localhost/daily-2016-01-01"),
vars: map[string]string{"type": "daily", "date": "2016-01-01"},
host: "",
path: "/daily-2016-01-01",
pathTemplate: `/{type:(?i:daily|mini|variety)}-{date:\d{4,4}-\d{2,2}-\d{2,2}}`,
shouldMatch: true,
}, },
} }
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
@@ -382,108 +417,126 @@ func TestPathPrefix(t *testing.T) {
shouldMatch: false, shouldMatch: false,
}, },
{ {
title: "PathPrefix route with pattern, match", title: "PathPrefix route with pattern, match",
route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/222/333"), request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "222"}, vars: map[string]string{"v1": "222"},
host: "", host: "",
path: "/111/222", path: "/111/222",
shouldMatch: true, pathTemplate: `/111/{v1:[0-9]{3}}`,
shouldMatch: true,
}, },
{ {
title: "PathPrefix route with pattern, URL prefix in request does not match", title: "PathPrefix route with pattern, URL prefix in request does not match",
route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/aaa/333"), request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "222"}, vars: map[string]string{"v1": "222"},
host: "", host: "",
path: "/111/222", path: "/111/222",
shouldMatch: false, pathTemplate: `/111/{v1:[0-9]{3}}`,
shouldMatch: false,
}, },
{ {
title: "PathPrefix route with multiple patterns, match", title: "PathPrefix route with multiple patterns, match",
route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/222/333"), request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "111", "v2": "222"}, vars: map[string]string{"v1": "111", "v2": "222"},
host: "", host: "",
path: "/111/222", path: "/111/222",
shouldMatch: true, pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`,
shouldMatch: true,
}, },
{ {
title: "PathPrefix route with multiple patterns, URL prefix in request does not match", title: "PathPrefix route with multiple patterns, URL prefix in request does not match",
route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/aaa/333"), request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "111", "v2": "222"}, vars: map[string]string{"v1": "111", "v2": "222"},
host: "", host: "",
path: "/111/222", path: "/111/222",
shouldMatch: false, pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`,
shouldMatch: false,
}, },
} }
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
func TestHostPath(t *testing.T) { func TestHostPath(t *testing.T) {
tests := []routeTest{ tests := []routeTest{
{ {
title: "Host and Path route, match", title: "Host and Path route, match",
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{}, vars: map[string]string{},
host: "", host: "",
path: "", path: "",
shouldMatch: true, pathTemplate: `/111/222/333`,
hostTemplate: `aaa.bbb.ccc`,
shouldMatch: true,
}, },
{ {
title: "Host and Path route, wrong host in request URL", title: "Host and Path route, wrong host in request URL",
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{}, vars: map[string]string{},
host: "", host: "",
path: "", path: "",
shouldMatch: false, pathTemplate: `/111/222/333`,
hostTemplate: `aaa.bbb.ccc`,
shouldMatch: false,
}, },
{ {
title: "Host and Path route with pattern, match", title: "Host and Path route with pattern, match",
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb", "v2": "222"}, vars: map[string]string{"v1": "bbb", "v2": "222"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "/111/222/333", path: "/111/222/333",
shouldMatch: true, pathTemplate: `/111/{v2:[0-9]{3}}/333`,
hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`,
shouldMatch: true,
}, },
{ {
title: "Host and Path route with pattern, URL in request does not match", title: "Host and Path route with pattern, URL in request does not match",
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb", "v2": "222"}, vars: map[string]string{"v1": "bbb", "v2": "222"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "/111/222/333", path: "/111/222/333",
shouldMatch: false, pathTemplate: `/111/{v2:[0-9]{3}}/333`,
hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`,
shouldMatch: false,
}, },
{ {
title: "Host and Path route with multiple patterns, match", title: "Host and Path route with multiple patterns, match",
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "/111/222/333", path: "/111/222/333",
shouldMatch: true, pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`,
hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`,
shouldMatch: true,
}, },
{ {
title: "Host and Path route with multiple patterns, URL in request does not match", title: "Host and Path route with multiple patterns, URL in request does not match",
route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"},
host: "aaa.bbb.ccc", host: "aaa.bbb.ccc",
path: "/111/222/333", path: "/111/222/333",
shouldMatch: false, pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`,
hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`,
shouldMatch: false,
}, },
} }
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
@@ -541,6 +594,7 @@ func TestHeaders(t *testing.T) {
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
@@ -578,6 +632,7 @@ func TestMethods(t *testing.T) {
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
@@ -593,22 +648,26 @@ func TestQueries(t *testing.T) {
shouldMatch: true, shouldMatch: true,
}, },
{ {
title: "Queries route, match with a query string", title: "Queries route, match with a query string",
route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"),
request: newRequest("GET", "http://www.example.com/api?foo=bar&baz=ding"), request: newRequest("GET", "http://www.example.com/api?foo=bar&baz=ding"),
vars: map[string]string{}, vars: map[string]string{},
host: "", host: "",
path: "", path: "",
shouldMatch: true, pathTemplate: `/api`,
hostTemplate: `www.example.com`,
shouldMatch: true,
}, },
{ {
title: "Queries route, match with a query string out of order", title: "Queries route, match with a query string out of order",
route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"),
request: newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"), request: newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"),
vars: map[string]string{}, vars: map[string]string{},
host: "", host: "",
path: "", path: "",
shouldMatch: true, pathTemplate: `/api`,
hostTemplate: `www.example.com`,
shouldMatch: true,
}, },
{ {
title: "Queries route, bad query", title: "Queries route, bad query",
@@ -803,6 +862,7 @@ func TestQueries(t *testing.T) {
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
@@ -839,6 +899,7 @@ func TestSchemes(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
@@ -873,6 +934,7 @@ func TestMatcherFunc(t *testing.T) {
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
@@ -885,9 +947,10 @@ func TestBuildVarsFunc(t *testing.T) {
vars["v2"] = "a" vars["v2"] = "a"
return vars return vars
}), }),
request: newRequest("GET", "http://localhost/111/2"), request: newRequest("GET", "http://localhost/111/2"),
path: "/111/3a", path: "/111/3a",
shouldMatch: true, pathTemplate: `/111/{v1:\d}{v2:.*}`,
shouldMatch: true,
}, },
{ {
title: "BuildVarsFunc set on route and parent route", title: "BuildVarsFunc set on route and parent route",
@@ -898,14 +961,16 @@ func TestBuildVarsFunc(t *testing.T) {
vars["v2"] = "b" vars["v2"] = "b"
return vars return vars
}), }),
request: newRequest("GET", "http://localhost/1/a"), request: newRequest("GET", "http://localhost/1/a"),
path: "/2/b", path: "/2/b",
shouldMatch: true, pathTemplate: `/{v1:\d}/{v2:\w}`,
shouldMatch: true,
}, },
} }
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
@@ -915,41 +980,48 @@ func TestSubRouter(t *testing.T) {
tests := []routeTest{ tests := []routeTest{
{ {
route: subrouter1.Path("/{v2:[a-z]+}"), route: subrouter1.Path("/{v2:[a-z]+}"),
request: newRequest("GET", "http://aaa.google.com/bbb"), request: newRequest("GET", "http://aaa.google.com/bbb"),
vars: map[string]string{"v1": "aaa", "v2": "bbb"}, vars: map[string]string{"v1": "aaa", "v2": "bbb"},
host: "aaa.google.com", host: "aaa.google.com",
path: "/bbb", path: "/bbb",
shouldMatch: true, pathTemplate: `/{v2:[a-z]+}`,
hostTemplate: `{v1:[a-z]+}.google.com`,
shouldMatch: true,
}, },
{ {
route: subrouter1.Path("/{v2:[a-z]+}"), route: subrouter1.Path("/{v2:[a-z]+}"),
request: newRequest("GET", "http://111.google.com/111"), request: newRequest("GET", "http://111.google.com/111"),
vars: map[string]string{"v1": "aaa", "v2": "bbb"}, vars: map[string]string{"v1": "aaa", "v2": "bbb"},
host: "aaa.google.com", host: "aaa.google.com",
path: "/bbb", path: "/bbb",
shouldMatch: false, pathTemplate: `/{v2:[a-z]+}`,
hostTemplate: `{v1:[a-z]+}.google.com`,
shouldMatch: false,
}, },
{ {
route: subrouter2.Path("/baz/{v2}"), route: subrouter2.Path("/baz/{v2}"),
request: newRequest("GET", "http://localhost/foo/bar/baz/ding"), request: newRequest("GET", "http://localhost/foo/bar/baz/ding"),
vars: map[string]string{"v1": "bar", "v2": "ding"}, vars: map[string]string{"v1": "bar", "v2": "ding"},
host: "", host: "",
path: "/foo/bar/baz/ding", path: "/foo/bar/baz/ding",
shouldMatch: true, pathTemplate: `/foo/{v1}/baz/{v2}`,
shouldMatch: true,
}, },
{ {
route: subrouter2.Path("/baz/{v2}"), route: subrouter2.Path("/baz/{v2}"),
request: newRequest("GET", "http://localhost/foo/bar"), request: newRequest("GET", "http://localhost/foo/bar"),
vars: map[string]string{"v1": "bar", "v2": "ding"}, vars: map[string]string{"v1": "bar", "v2": "ding"},
host: "", host: "",
path: "/foo/bar/baz/ding", path: "/foo/bar/baz/ding",
shouldMatch: false, pathTemplate: `/foo/{v1}/baz/{v2}`,
shouldMatch: false,
}, },
} }
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
@@ -1045,6 +1117,7 @@ func TestStrictSlash(t *testing.T) {
for _, test := range tests { for _, test := range tests {
testRoute(t, test) testRoute(t, test)
testTemplate(t, test)
} }
} }
@@ -1123,6 +1196,42 @@ func TestWalkNested(t *testing.T) {
} }
} }
func TestWalkErrorRoute(t *testing.T) {
router := NewRouter()
router.Path("/g")
expectedError := errors.New("error")
err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error {
return expectedError
})
if err != expectedError {
t.Errorf("Expected %v routes, found %v", expectedError, err)
}
}
func TestWalkErrorMatcher(t *testing.T) {
router := NewRouter()
expectedError := router.Path("/g").Subrouter().Path("").GetError()
err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error {
return route.GetError()
})
if err != expectedError {
t.Errorf("Expected %v routes, found %v", expectedError, err)
}
}
func TestWalkErrorHandler(t *testing.T) {
handler := NewRouter()
expectedError := handler.Path("/path").Subrouter().Path("").GetError()
router := NewRouter()
router.Path("/g").Handler(handler)
err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error {
return route.GetError()
})
if err != expectedError {
t.Errorf("Expected %v routes, found %v", expectedError, err)
}
}
func TestSubrouterErrorHandling(t *testing.T) { func TestSubrouterErrorHandling(t *testing.T) {
superRouterCalled := false superRouterCalled := false
subRouterCalled := false subRouterCalled := false
@@ -1152,14 +1261,13 @@ func TestSubrouterErrorHandling(t *testing.T) {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
func getRouteTemplate(route *Route) string { func getRouteTemplate(route *Route) string {
host, path := "none", "none" host, err := route.GetHostTemplate()
if route.regexp != nil { if err != nil {
if route.regexp.host != nil { host = "none"
host = route.regexp.host.template }
} path, err := route.GetPathTemplate()
if route.regexp.path != nil { if err != nil {
path = route.regexp.path.template path = "none"
}
} }
return fmt.Sprintf("Host: %v, Path: %v", host, path) return fmt.Sprintf("Host: %v, Path: %v", host, path)
} }
@@ -1221,34 +1329,26 @@ func testRoute(t *testing.T, test routeTest) {
} }
} }
// Tests that the context is cleared or not cleared properly depending on func testTemplate(t *testing.T, test routeTest) {
// the configuration of the router route := test.route
func TestKeepContext(t *testing.T) { pathTemplate := test.pathTemplate
func1 := func(w http.ResponseWriter, r *http.Request) {} if len(pathTemplate) == 0 {
pathTemplate = test.path
r := NewRouter() }
r.HandleFunc("/", func1).Name("func1") hostTemplate := test.hostTemplate
if len(hostTemplate) == 0 {
req, _ := http.NewRequest("GET", "http://localhost/", nil) hostTemplate = test.host
context.Set(req, "t", 1)
res := new(http.ResponseWriter)
r.ServeHTTP(*res, req)
if _, ok := context.GetOk(req, "t"); ok {
t.Error("Context should have been cleared at end of request")
} }
r.KeepContext = true routePathTemplate, pathErr := route.GetPathTemplate()
if pathErr == nil && routePathTemplate != pathTemplate {
req, _ = http.NewRequest("GET", "http://localhost/", nil) t.Errorf("(%v) GetPathTemplate not equal: expected %v, got %v", test.title, pathTemplate, routePathTemplate)
context.Set(req, "t", 1)
r.ServeHTTP(*res, req)
if _, ok := context.GetOk(req, "t"); !ok {
t.Error("Context should NOT have been cleared at end of request")
} }
routeHostTemplate, hostErr := route.GetHostTemplate()
if hostErr == nil && routeHostTemplate != hostTemplate {
t.Errorf("(%v) GetHostTemplate not equal: expected %v, got %v", test.title, hostTemplate, routeHostTemplate)
}
} }
type TestA301ResponseWriter struct { type TestA301ResponseWriter struct {
@@ -1291,6 +1391,24 @@ func Test301Redirect(t *testing.T) {
} }
} }
func TestSkipClean(t *testing.T) {
func1 := func(w http.ResponseWriter, r *http.Request) {}
func2 := func(w http.ResponseWriter, r *http.Request) {}
r := NewRouter()
r.SkipClean(true)
r.HandleFunc("/api/", func2).Name("func2")
r.HandleFunc("/", func1).Name("func1")
req, _ := http.NewRequest("GET", "http://localhost//api/?abc=def", nil)
res := NewRecorder()
r.ServeHTTP(res, req)
if len(res.HeaderMap["Location"]) != 0 {
t.Errorf("Shouldn't redirect since skip clean is disabled")
}
}
// https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW // https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW
func TestSubrouterHeader(t *testing.T) { func TestSubrouterHeader(t *testing.T) {
expected := "func1 response" expected := "func1 response"
+3 -7
View File
@@ -36,10 +36,6 @@ func NewRecorder() *ResponseRecorder {
} }
} }
// DefaultRemoteAddr is the default remote address to return in RemoteAddr if
// an explicit DefaultRemoteAddr isn't set on ResponseRecorder.
const DefaultRemoteAddr = "1.2.3.4"
// Header returns the response headers. // Header returns the response headers.
func (rw *ResponseRecorder) Header() http.Header { func (rw *ResponseRecorder) Header() http.Header {
return rw.HeaderMap return rw.HeaderMap
@@ -576,10 +572,10 @@ func TestSubRouting(t *testing.T) {
} }
u, _ := router.Get("products").URL() u, _ := router.Get("products").URL()
builtUrl := u.String() builtURL := u.String()
// Yay, subroute aware of the domain when building! // Yay, subroute aware of the domain when building!
if builtUrl != url { if builtURL != url {
t.Errorf("Expected %q, got %q.", url, builtUrl) t.Errorf("Expected %q, got %q.", url, builtURL)
} }
} }
+35 -40
View File
@@ -73,14 +73,14 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash
tpl[idxs[i]:end]) tpl[idxs[i]:end])
} }
// Build the regexp pattern. // Build the regexp pattern.
varIdx := i / 2 fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt)
fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(varIdx), patt)
// Build the reverse template. // Build the reverse template.
fmt.Fprintf(reverse, "%s%%s", raw) fmt.Fprintf(reverse, "%s%%s", raw)
// Append variable name and compiled pattern. // Append variable name and compiled pattern.
varsN[varIdx] = name varsN[i/2] = name
varsR[varIdx], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -148,10 +148,11 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
if !r.matchHost { if !r.matchHost {
if r.matchQuery { if r.matchQuery {
return r.matchQueryString(req) return r.matchQueryString(req)
} else {
return r.regexp.MatchString(req.URL.Path)
} }
return r.regexp.MatchString(req.URL.Path)
} }
return r.regexp.MatchString(getHost(req)) return r.regexp.MatchString(getHost(req))
} }
@@ -181,10 +182,10 @@ func (r *routeRegexp) url(values map[string]string) (string, error) {
return rv, nil return rv, nil
} }
// getUrlQuery returns a single query parameter from a request URL. // getURLQuery returns a single query parameter from a request URL.
// For a URL with foo=bar&baz=ding, we return only the relevant key // For a URL with foo=bar&baz=ding, we return only the relevant key
// value pair for the routeRegexp. // value pair for the routeRegexp.
func (r *routeRegexp) getUrlQuery(req *http.Request) string { func (r *routeRegexp) getURLQuery(req *http.Request) string {
if !r.matchQuery { if !r.matchQuery {
return "" return ""
} }
@@ -198,14 +199,14 @@ func (r *routeRegexp) getUrlQuery(req *http.Request) string {
} }
func (r *routeRegexp) matchQueryString(req *http.Request) bool { func (r *routeRegexp) matchQueryString(req *http.Request) bool {
return r.regexp.MatchString(r.getUrlQuery(req)) return r.regexp.MatchString(r.getURLQuery(req))
} }
// braceIndices returns the first level curly brace indices from a string. // braceIndices returns the first level curly brace indices from a string.
// It returns an error in case of unbalanced braces. // It returns an error in case of unbalanced braces.
func braceIndices(s string) ([]int, error) { func braceIndices(s string) ([]int, error) {
var level, idx int var level, idx int
idxs := make([]int, 0) var idxs []int
for i := 0; i < len(s); i++ { for i := 0; i < len(s); i++ {
switch s[i] { switch s[i] {
case '{': case '{':
@@ -246,30 +247,17 @@ type routeRegexpGroup struct {
func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
// Store host variables. // Store host variables.
if v.host != nil { if v.host != nil {
hostVars := v.host.regexp.FindStringSubmatch(getHost(req)) host := getHost(req)
if hostVars != nil { matches := v.host.regexp.FindStringSubmatchIndex(host)
subexpNames := v.host.regexp.SubexpNames() if len(matches) > 0 {
varName := 0 extractVars(host, matches, v.host.varsN, m.Vars)
for i, name := range subexpNames[1:] {
if name != "" && name == varGroupName(varName) {
m.Vars[v.host.varsN[varName]] = hostVars[i+1]
varName++
}
}
} }
} }
// Store path variables. // Store path variables.
if v.path != nil { if v.path != nil {
pathVars := v.path.regexp.FindStringSubmatch(req.URL.Path) matches := v.path.regexp.FindStringSubmatchIndex(req.URL.Path)
if pathVars != nil { if len(matches) > 0 {
subexpNames := v.path.regexp.SubexpNames() extractVars(req.URL.Path, matches, v.path.varsN, m.Vars)
varName := 0
for i, name := range subexpNames[1:] {
if name != "" && name == varGroupName(varName) {
m.Vars[v.path.varsN[varName]] = pathVars[i+1]
varName++
}
}
// Check if we should redirect. // Check if we should redirect.
if v.path.strictSlash { if v.path.strictSlash {
p1 := strings.HasSuffix(req.URL.Path, "/") p1 := strings.HasSuffix(req.URL.Path, "/")
@@ -288,16 +276,10 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
} }
// Store query string variables. // Store query string variables.
for _, q := range v.queries { for _, q := range v.queries {
queryVars := q.regexp.FindStringSubmatch(q.getUrlQuery(req)) queryURL := q.getURLQuery(req)
if queryVars != nil { matches := q.regexp.FindStringSubmatchIndex(queryURL)
subexpNames := q.regexp.SubexpNames() if len(matches) > 0 {
varName := 0 extractVars(queryURL, matches, q.varsN, m.Vars)
for i, name := range subexpNames[1:] {
if name != "" && name == varGroupName(varName) {
m.Vars[q.varsN[varName]] = queryVars[i+1]
varName++
}
}
} }
} }
} }
@@ -315,3 +297,16 @@ func getHost(r *http.Request) string {
return host return host
} }
func extractVars(input string, matches []int, names []string, output map[string]string) {
matchesCount := 0
prevEnd := -1
for i := 2; i < len(matches) && matchesCount < len(names); i += 2 {
if prevEnd < matches[i+1] {
value := input[matches[i]:matches[i+1]]
output[names[matchesCount]] = value
prevEnd = matches[i+1]
matchesCount++
}
}
}
+41 -2
View File
@@ -26,6 +26,9 @@ type Route struct {
// If true, when the path pattern is "/path/", accessing "/path" will // If true, when the path pattern is "/path/", accessing "/path" will
// redirect to the former and vice versa. // redirect to the former and vice versa.
strictSlash bool strictSlash bool
// If true, when the path pattern is "/path//to", accessing "/path//to"
// will not redirect
skipClean bool
// If true, this route never matches: it is only used to build URLs. // If true, this route never matches: it is only used to build URLs.
buildOnly bool buildOnly bool
// The name used to build URLs. // The name used to build URLs.
@@ -36,6 +39,10 @@ type Route struct {
buildVarsFunc BuildVarsFunc buildVarsFunc BuildVarsFunc
} }
func (r *Route) SkipClean() bool {
return r.skipClean
}
// Match matches the route against the request. // Match matches the route against the request.
func (r *Route) Match(req *http.Request, match *RouteMatch) bool { func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
if r.buildOnly || r.err != nil { if r.buildOnly || r.err != nil {
@@ -217,8 +224,9 @@ func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool {
return matchMapWithRegex(m, r.Header, true) return matchMapWithRegex(m, r.Header, true)
} }
// Regular expressions can be used with headers as well. // HeadersRegexp accepts a sequence of key/value pairs, where the value has regex
// It accepts a sequence of key/value pairs, where the value has regex support. For example // support. For example:
//
// r := mux.NewRouter() // r := mux.NewRouter()
// r.HeadersRegexp("Content-Type", "application/(text|json)", // r.HeadersRegexp("Content-Type", "application/(text|json)",
// "X-Requested-With", "XMLHttpRequest") // "X-Requested-With", "XMLHttpRequest")
@@ -263,6 +271,7 @@ func (r *Route) Host(tpl string) *Route {
// MatcherFunc is the function signature used by custom matchers. // MatcherFunc is the function signature used by custom matchers.
type MatcherFunc func(*http.Request, *RouteMatch) bool type MatcherFunc func(*http.Request, *RouteMatch) bool
// Match returns the match for a given request.
func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool {
return m(r, match) return m(r, match)
} }
@@ -532,6 +541,36 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
}, nil }, nil
} }
// GetPathTemplate returns the template used to build the
// route match.
// This is useful for building simple REST API documentation and for instrumentation
// against third-party services.
// An error will be returned if the route does not define a path.
func (r *Route) GetPathTemplate() (string, error) {
if r.err != nil {
return "", r.err
}
if r.regexp == nil || r.regexp.path == nil {
return "", errors.New("mux: route doesn't have a path")
}
return r.regexp.path.template, nil
}
// GetHostTemplate returns the template used to build the
// route match.
// This is useful for building simple REST API documentation and for instrumentation
// against third-party services.
// An error will be returned if the route does not define a host.
func (r *Route) GetHostTemplate() (string, error) {
if r.err != nil {
return "", r.err
}
if r.regexp == nil || r.regexp.host == nil {
return "", errors.New("mux: route doesn't have a host")
}
return r.regexp.host.template, nil
}
// prepareVars converts the route variable pairs into a map. If the route has a // prepareVars converts the route variable pairs into a map. If the route has a
// BuildVarsFunc, it is invoked. // BuildVarsFunc, it is invoked.
func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { func (r *Route) prepareVars(pairs ...string) (map[string]string, error) {
+3
View File
@@ -20,3 +20,6 @@ _cgo_export.*
_testmain.go _testmain.go
*.exe *.exe
.idea/
*.iml
+15 -4
View File
@@ -1,6 +1,17 @@
language: go language: go
sudo: false
go: matrix:
- 1.1 include:
- 1.2 - go: 1.4
- tip - go: 1.5
- go: 1.6
- go: tip
allow_failures:
- go: tip
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d .)
- go vet $(go list ./... | grep -v /vendor/)
- go test -v -race ./...
+31 -6
View File
@@ -298,12 +298,8 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
} }
if u.Scheme == "https" { if u.Scheme == "https" {
cfg := d.TLSClientConfig cfg := cloneTLSConfig(d.TLSClientConfig)
if cfg == nil { if cfg.ServerName == "" {
cfg = &tls.Config{ServerName: hostNoPort}
} else if cfg.ServerName == "" {
shallowCopy := *cfg
cfg = &shallowCopy
cfg.ServerName = hostNoPort cfg.ServerName = hostNoPort
} }
tlsConn := tls.Client(netConn, cfg) tlsConn := tls.Client(netConn, cfg)
@@ -348,3 +344,32 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
netConn = nil // to avoid close in defer. netConn = nil // to avoid close in defer.
return conn, resp, nil return conn, resp, nil
} }
// cloneTLSConfig clones all public fields except the fields
// SessionTicketsDisabled and SessionTicketKey. This avoids copying the
// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a
// config in active use.
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return &tls.Config{
Rand: cfg.Rand,
Time: cfg.Time,
Certificates: cfg.Certificates,
NameToCertificate: cfg.NameToCertificate,
GetCertificate: cfg.GetCertificate,
RootCAs: cfg.RootCAs,
NextProtos: cfg.NextProtos,
ServerName: cfg.ServerName,
ClientAuth: cfg.ClientAuth,
ClientCAs: cfg.ClientCAs,
InsecureSkipVerify: cfg.InsecureSkipVerify,
CipherSuites: cfg.CipherSuites,
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
ClientSessionCache: cfg.ClientSessionCache,
MinVersion: cfg.MinVersion,
MaxVersion: cfg.MaxVersion,
CurvePreferences: cfg.CurvePreferences,
}
}
+85
View File
@@ -0,0 +1,85 @@
// Copyright 2016 The Gorilla WebSocket 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 websocket
import (
"compress/flate"
"errors"
"io"
"strings"
)
func decompressNoContextTakeover(r io.Reader) io.Reader {
const tail =
// Add four bytes as specified in RFC
"\x00\x00\xff\xff" +
// Add final block to squelch unexpected EOF error from flate reader.
"\x01\x00\x00\xff\xff"
return flate.NewReader(io.MultiReader(r, strings.NewReader(tail)))
}
func compressNoContextTakeover(w io.WriteCloser) (io.WriteCloser, error) {
tw := &truncWriter{w: w}
fw, err := flate.NewWriter(tw, 3)
return &flateWrapper{fw: fw, tw: tw}, err
}
// truncWriter is an io.Writer that writes all but the last four bytes of the
// stream to another io.Writer.
type truncWriter struct {
w io.WriteCloser
n int
p [4]byte
}
func (w *truncWriter) Write(p []byte) (int, error) {
n := 0
// fill buffer first for simplicity.
if w.n < len(w.p) {
n = copy(w.p[w.n:], p)
p = p[n:]
w.n += n
if len(p) == 0 {
return n, nil
}
}
m := len(p)
if m > len(w.p) {
m = len(w.p)
}
if nn, err := w.w.Write(w.p[:m]); err != nil {
return n + nn, err
}
copy(w.p[:], w.p[m:])
copy(w.p[len(w.p)-m:], p[len(p)-m:])
nn, err := w.w.Write(p[:len(p)-m])
return n + nn, err
}
type flateWrapper struct {
fw *flate.Writer
tw *truncWriter
}
func (w *flateWrapper) Write(p []byte) (int, error) {
return w.fw.Write(p)
}
func (w *flateWrapper) Close() error {
err1 := w.fw.Flush()
if w.tw.p != [4]byte{0, 0, 0xff, 0xff} {
return errors.New("websocket: internal error, unexpected bytes at end of flate stream")
}
err2 := w.tw.w.Close()
if err1 != nil {
return err1
}
return err2
}
+31
View File
@@ -0,0 +1,31 @@
package websocket
import (
"bytes"
"io"
"testing"
)
type nopCloser struct{ io.Writer }
func (nopCloser) Close() error { return nil }
func TestTruncWriter(t *testing.T) {
const data = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijlkmnopqrstuvwxyz987654321"
for n := 1; n <= 10; n++ {
var b bytes.Buffer
w := &truncWriter{w: nopCloser{&b}}
p := []byte(data)
for len(p) > 0 {
m := len(p)
if m > n {
m = n
}
w.Write(p[:m])
p = p[m:]
}
if b.String() != data[:len(data)-len(w.p)] {
t.Errorf("%d: %q", n, b.String())
}
}
}
+186 -107
View File
@@ -14,14 +14,23 @@ import (
"net" "net"
"strconv" "strconv"
"time" "time"
"unicode/utf8"
) )
const ( const (
// Frame header byte 0 bits from Section 5.2 of RFC 6455
finalBit = 1 << 7
rsv1Bit = 1 << 6
rsv2Bit = 1 << 5
rsv3Bit = 1 << 4
// Frame header byte 1 bits from Section 5.2 of RFC 6455
maskBit = 1 << 7
maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask
maxControlFramePayloadSize = 125 maxControlFramePayloadSize = 125
finalBit = 1 << 7
maskBit = 1 << 7 writeWait = time.Second
writeWait = time.Second
defaultReadBufferSize = 4096 defaultReadBufferSize = 4096
defaultWriteBufferSize = 4096 defaultWriteBufferSize = 4096
@@ -43,6 +52,8 @@ const (
CloseMessageTooBig = 1009 CloseMessageTooBig = 1009
CloseMandatoryExtension = 1010 CloseMandatoryExtension = 1010
CloseInternalServerErr = 1011 CloseInternalServerErr = 1011
CloseServiceRestart = 1012
CloseTryAgainLater = 1013
CloseTLSHandshake = 1015 CloseTLSHandshake = 1015
) )
@@ -184,6 +195,29 @@ func isData(frameType int) bool {
return frameType == TextMessage || frameType == BinaryMessage return frameType == TextMessage || frameType == BinaryMessage
} }
var validReceivedCloseCodes = map[int]bool{
// see http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
CloseNormalClosure: true,
CloseGoingAway: true,
CloseProtocolError: true,
CloseUnsupportedData: true,
CloseNoStatusReceived: false,
CloseAbnormalClosure: false,
CloseInvalidFramePayloadData: true,
ClosePolicyViolation: true,
CloseMessageTooBig: true,
CloseMandatoryExtension: true,
CloseInternalServerErr: true,
CloseServiceRestart: true,
CloseTryAgainLater: true,
CloseTLSHandshake: false,
}
func isValidReceivedCloseCode(code int) bool {
return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999)
}
func maskBytes(key [4]byte, pos int, b []byte) int { func maskBytes(key [4]byte, pos int, b []byte) int {
for i := range b { for i := range b {
b[i] ^= key[pos&3] b[i] ^= key[pos&3]
@@ -204,24 +238,26 @@ type Conn struct {
subprotocol string subprotocol string
// Write fields // Write fields
mu chan bool // used as mutex to protect write to conn and closeSent mu chan bool // used as mutex to protect write to conn and closeSent
closeSent bool // true if close message was sent closeSent bool // whether close message was sent
// Message writer fields.
writeErr error writeErr error
writeBuf []byte // frame is constructed in this buffer. writeBuf []byte // frame is constructed in this buffer.
writePos int // end of data in writeBuf. writePos int // end of data in writeBuf.
writeFrameType int // type of the current frame. writeFrameType int // type of the current frame.
writeSeq int // incremented to invalidate message writers.
writeDeadline time.Time writeDeadline time.Time
isWriting bool // for best-effort concurrent write detection messageWriter *messageWriter // the current low-level message writer
writer io.WriteCloser // the current writer returned to the application
isWriting bool // for best-effort concurrent write detection
enableWriteCompression bool
writeCompress bool // whether next call to flushFrame should set RSV1
newCompressionWriter func(io.WriteCloser) (io.WriteCloser, error)
// Read fields // Read fields
readErr error readErr error
br *bufio.Reader br *bufio.Reader
readRemaining int64 // bytes remaining in current frame. readRemaining int64 // bytes remaining in current frame.
readFinal bool // true the current message has more frames. readFinal bool // true the current message has more frames.
readSeq int // incremented to invalidate message readers.
readLength int64 // Message size. readLength int64 // Message size.
readLimit int64 // Maximum message size. readLimit int64 // Maximum message size.
readMaskPos int readMaskPos int
@@ -229,6 +265,10 @@ type Conn struct {
handlePong func(string) error handlePong func(string) error
handlePing func(string) error handlePing func(string) error
readErrCount int readErrCount int
messageReader *messageReader // the current low-level reader
readDecompress bool // whether last read frame had RSV1 set
newDecompressionReader func(io.Reader) io.Reader
} }
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn { func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn {
@@ -238,19 +278,23 @@ func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int)
if readBufferSize == 0 { if readBufferSize == 0 {
readBufferSize = defaultReadBufferSize readBufferSize = defaultReadBufferSize
} }
if readBufferSize < maxControlFramePayloadSize {
readBufferSize = maxControlFramePayloadSize
}
if writeBufferSize == 0 { if writeBufferSize == 0 {
writeBufferSize = defaultWriteBufferSize writeBufferSize = defaultWriteBufferSize
} }
c := &Conn{ c := &Conn{
isServer: isServer, isServer: isServer,
br: bufio.NewReaderSize(conn, readBufferSize), br: bufio.NewReaderSize(conn, readBufferSize),
conn: conn, conn: conn,
mu: mu, mu: mu,
readFinal: true, readFinal: true,
writeBuf: make([]byte, writeBufferSize+maxFrameHeaderSize), writeBuf: make([]byte, writeBufferSize+maxFrameHeaderSize),
writeFrameType: noFrame, writeFrameType: noFrame,
writePos: maxFrameHeaderSize, writePos: maxFrameHeaderSize,
enableWriteCompression: true,
} }
c.SetPingHandler(nil) c.SetPingHandler(nil)
c.SetPongHandler(nil) c.SetPongHandler(nil)
@@ -364,8 +408,8 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
return hideTempErr(err) return hideTempErr(err)
} }
// NextWriter returns a writer for the next message to send. The writer's // NextWriter returns a writer for the next message to send. The writer's Close
// Close method flushes the complete message to the network. // method flushes the complete message to the network.
// //
// There can be at most one open writer on a connection. NextWriter closes the // There can be at most one open writer on a connection. NextWriter closes the
// previous writer if the application has not already done so. // previous writer if the application has not already done so.
@@ -374,8 +418,12 @@ func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
return nil, c.writeErr return nil, c.writeErr
} }
if c.writeFrameType != noFrame { // Close previous writer if not already closed by the application. It's
if err := c.flushFrame(true, nil); err != nil { // probably better to return an error in this situation, but we cannot
// change this without breaking existing applications.
if c.writer != nil {
err := c.writer.Close()
if err != nil {
return nil, err return nil, err
} }
} }
@@ -385,16 +433,32 @@ func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
} }
c.writeFrameType = messageType c.writeFrameType = messageType
return messageWriter{c, c.writeSeq}, nil c.messageWriter = &messageWriter{c}
var w io.WriteCloser = c.messageWriter
if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) {
c.writeCompress = true
var err error
w, err = c.newCompressionWriter(w)
if err != nil {
c.writer.Close()
return nil, err
}
}
return w, nil
} }
// flushFrame writes buffered data and extra as a frame to the network. The
// final argument indicates that this is the last frame in the message.
func (c *Conn) flushFrame(final bool, extra []byte) error { func (c *Conn) flushFrame(final bool, extra []byte) error {
length := c.writePos - maxFrameHeaderSize + len(extra) length := c.writePos - maxFrameHeaderSize + len(extra)
// Check for invalid control frames. // Check for invalid control frames.
if isControl(c.writeFrameType) && if isControl(c.writeFrameType) &&
(!final || length > maxControlFramePayloadSize) { (!final || length > maxControlFramePayloadSize) {
c.writeSeq++ c.messageWriter = nil
c.writer = nil
c.writeFrameType = noFrame c.writeFrameType = noFrame
c.writePos = maxFrameHeaderSize c.writePos = maxFrameHeaderSize
return errInvalidControlFrame return errInvalidControlFrame
@@ -404,6 +468,11 @@ func (c *Conn) flushFrame(final bool, extra []byte) error {
if final { if final {
b0 |= finalBit b0 |= finalBit
} }
if c.writeCompress {
b0 |= rsv1Bit
}
c.writeCompress = false
b1 := byte(0) b1 := byte(0)
if !c.isServer { if !c.isServer {
b1 |= maskBit b1 |= maskBit
@@ -462,20 +531,18 @@ func (c *Conn) flushFrame(final bool, extra []byte) error {
c.writePos = maxFrameHeaderSize c.writePos = maxFrameHeaderSize
c.writeFrameType = continuationFrame c.writeFrameType = continuationFrame
if final { if final {
c.writeSeq++ c.messageWriter = nil
c.writer = nil
c.writeFrameType = noFrame c.writeFrameType = noFrame
} }
return c.writeErr return c.writeErr
} }
type messageWriter struct { type messageWriter struct{ c *Conn }
c *Conn
seq int
}
func (w messageWriter) err() error { func (w *messageWriter) err() error {
c := w.c c := w.c
if c.writeSeq != w.seq { if c.messageWriter != w {
return errWriteClosed return errWriteClosed
} }
if c.writeErr != nil { if c.writeErr != nil {
@@ -484,7 +551,7 @@ func (w messageWriter) err() error {
return nil return nil
} }
func (w messageWriter) ncopy(max int) (int, error) { func (w *messageWriter) ncopy(max int) (int, error) {
n := len(w.c.writeBuf) - w.c.writePos n := len(w.c.writeBuf) - w.c.writePos
if n <= 0 { if n <= 0 {
if err := w.c.flushFrame(false, nil); err != nil { if err := w.c.flushFrame(false, nil); err != nil {
@@ -498,14 +565,14 @@ func (w messageWriter) ncopy(max int) (int, error) {
return n, nil return n, nil
} }
func (w messageWriter) write(final bool, p []byte) (int, error) { func (w *messageWriter) Write(p []byte) (int, error) {
if err := w.err(); err != nil { if err := w.err(); err != nil {
return 0, err return 0, err
} }
if len(p) > 2*len(w.c.writeBuf) && w.c.isServer { if len(p) > 2*len(w.c.writeBuf) && w.c.isServer {
// Don't buffer large messages. // Don't buffer large messages.
err := w.c.flushFrame(final, p) err := w.c.flushFrame(false, p)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -525,11 +592,7 @@ func (w messageWriter) write(final bool, p []byte) (int, error) {
return nn, nil return nn, nil
} }
func (w messageWriter) Write(p []byte) (int, error) { func (w *messageWriter) WriteString(p string) (int, error) {
return w.write(false, p)
}
func (w messageWriter) WriteString(p string) (int, error) {
if err := w.err(); err != nil { if err := w.err(); err != nil {
return 0, err return 0, err
} }
@@ -547,7 +610,7 @@ func (w messageWriter) WriteString(p string) (int, error) {
return nn, nil return nn, nil
} }
func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { func (w *messageWriter) ReadFrom(r io.Reader) (nn int64, err error) {
if err := w.err(); err != nil { if err := w.err(); err != nil {
return 0, err return 0, err
} }
@@ -572,7 +635,7 @@ func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) {
return nn, err return nn, err
} }
func (w messageWriter) Close() error { func (w *messageWriter) Close() error {
if err := w.err(); err != nil { if err := w.err(); err != nil {
return err return err
} }
@@ -582,20 +645,22 @@ func (w messageWriter) Close() error {
// WriteMessage is a helper method for getting a writer using NextWriter, // WriteMessage is a helper method for getting a writer using NextWriter,
// writing the message and closing the writer. // writing the message and closing the writer.
func (c *Conn) WriteMessage(messageType int, data []byte) error { func (c *Conn) WriteMessage(messageType int, data []byte) error {
wr, err := c.NextWriter(messageType) w, err := c.NextWriter(messageType)
if err != nil { if err != nil {
return err return err
} }
w := wr.(messageWriter) if _, ok := w.(*messageWriter); ok && c.isServer {
if _, err := w.write(true, data); err != nil { // Optimize write as a single frame.
n := copy(c.writeBuf[c.writePos:], data)
c.writePos += n
data = data[n:]
err = c.flushFrame(true, data)
return err return err
} }
if c.writeSeq == w.seq { if _, err = w.Write(data); err != nil {
if err := c.flushFrame(true, nil); err != nil { return err
return err
}
} }
return nil return w.Close()
} }
// SetWriteDeadline sets the write deadline on the underlying network // SetWriteDeadline sets the write deadline on the underlying network
@@ -609,22 +674,6 @@ func (c *Conn) SetWriteDeadline(t time.Time) error {
// Read methods // Read methods
// readFull is like io.ReadFull except that io.EOF is never returned.
func (c *Conn) readFull(p []byte) (err error) {
var n int
for n < len(p) && err == nil {
var nn int
nn, err = c.br.Read(p[n:])
n += nn
}
if n == len(p) {
err = nil
} else if err == io.EOF {
err = errUnexpectedEOF
}
return
}
func (c *Conn) advanceFrame() (int, error) { func (c *Conn) advanceFrame() (int, error) {
// 1. Skip remainder of previous frame. // 1. Skip remainder of previous frame.
@@ -637,19 +686,24 @@ func (c *Conn) advanceFrame() (int, error) {
// 2. Read and parse first two bytes of frame header. // 2. Read and parse first two bytes of frame header.
var b [8]byte p, err := c.read(2)
if err := c.readFull(b[:2]); err != nil { if err != nil {
return noFrame, err return noFrame, err
} }
final := b[0]&finalBit != 0 final := p[0]&finalBit != 0
frameType := int(b[0] & 0xf) frameType := int(p[0] & 0xf)
reserved := int((b[0] >> 4) & 0x7) mask := p[1]&maskBit != 0
mask := b[1]&maskBit != 0 c.readRemaining = int64(p[1] & 0x7f)
c.readRemaining = int64(b[1] & 0x7f)
if reserved != 0 { c.readDecompress = false
return noFrame, c.handleProtocolError("unexpected reserved bits " + strconv.Itoa(reserved)) if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 {
c.readDecompress = true
p[0] &^= rsv1Bit
}
if rsv := p[0] & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 {
return noFrame, c.handleProtocolError("unexpected reserved bits 0x" + strconv.FormatInt(int64(rsv), 16))
} }
switch frameType { switch frameType {
@@ -678,15 +732,17 @@ func (c *Conn) advanceFrame() (int, error) {
switch c.readRemaining { switch c.readRemaining {
case 126: case 126:
if err := c.readFull(b[:2]); err != nil { p, err := c.read(2)
if err != nil {
return noFrame, err return noFrame, err
} }
c.readRemaining = int64(binary.BigEndian.Uint16(b[:2])) c.readRemaining = int64(binary.BigEndian.Uint16(p))
case 127: case 127:
if err := c.readFull(b[:8]); err != nil { p, err := c.read(8)
if err != nil {
return noFrame, err return noFrame, err
} }
c.readRemaining = int64(binary.BigEndian.Uint64(b[:8])) c.readRemaining = int64(binary.BigEndian.Uint64(p))
} }
// 4. Handle frame masking. // 4. Handle frame masking.
@@ -697,9 +753,11 @@ func (c *Conn) advanceFrame() (int, error) {
if mask { if mask {
c.readMaskPos = 0 c.readMaskPos = 0
if err := c.readFull(c.readMaskKey[:]); err != nil { p, err := c.read(len(c.readMaskKey))
if err != nil {
return noFrame, err return noFrame, err
} }
copy(c.readMaskKey[:], p)
} }
// 5. For text and binary messages, enforce read limit and return. // 5. For text and binary messages, enforce read limit and return.
@@ -719,9 +777,9 @@ func (c *Conn) advanceFrame() (int, error) {
var payload []byte var payload []byte
if c.readRemaining > 0 { if c.readRemaining > 0 {
payload = make([]byte, c.readRemaining) payload, err = c.read(int(c.readRemaining))
c.readRemaining = 0 c.readRemaining = 0
if err := c.readFull(payload); err != nil { if err != nil {
return noFrame, err return noFrame, err
} }
if c.isServer { if c.isServer {
@@ -747,7 +805,13 @@ func (c *Conn) advanceFrame() (int, error) {
if len(payload) >= 2 { if len(payload) >= 2 {
echoMessage = payload[:2] echoMessage = payload[:2]
closeCode = int(binary.BigEndian.Uint16(payload)) closeCode = int(binary.BigEndian.Uint16(payload))
if !isValidReceivedCloseCode(closeCode) {
return noFrame, c.handleProtocolError("invalid close code")
}
closeText = string(payload[2:]) closeText = string(payload[2:])
if !utf8.ValidString(closeText) {
return noFrame, c.handleProtocolError("invalid utf8 payload in close frame")
}
} }
c.WriteControl(CloseMessage, echoMessage, time.Now().Add(writeWait)) c.WriteControl(CloseMessage, echoMessage, time.Now().Add(writeWait))
return noFrame, &CloseError{Code: closeCode, Text: closeText} return noFrame, &CloseError{Code: closeCode, Text: closeText}
@@ -773,7 +837,7 @@ func (c *Conn) handleProtocolError(message string) error {
// this method return the same error. // this method return the same error.
func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
c.readSeq++ c.messageReader = nil
c.readLength = 0 c.readLength = 0
for c.readErr == nil { for c.readErr == nil {
@@ -783,7 +847,12 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
break break
} }
if frameType == TextMessage || frameType == BinaryMessage { if frameType == TextMessage || frameType == BinaryMessage {
return frameType, messageReader{c, c.readSeq}, nil c.messageReader = &messageReader{c}
var r io.Reader = c.messageReader
if c.readDecompress {
r = c.newDecompressionReader(r)
}
return frameType, r, nil
} }
} }
@@ -798,48 +867,48 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
return noFrame, nil, c.readErr return noFrame, nil, c.readErr
} }
type messageReader struct { type messageReader struct{ c *Conn }
c *Conn
seq int
}
func (r messageReader) Read(b []byte) (int, error) { func (r *messageReader) Read(b []byte) (int, error) {
c := r.c
if r.seq != r.c.readSeq { if c.messageReader != r {
return 0, io.EOF return 0, io.EOF
} }
for r.c.readErr == nil { for c.readErr == nil {
if r.c.readRemaining > 0 { if c.readRemaining > 0 {
if int64(len(b)) > r.c.readRemaining { if int64(len(b)) > c.readRemaining {
b = b[:r.c.readRemaining] b = b[:c.readRemaining]
} }
n, err := r.c.br.Read(b) n, err := c.br.Read(b)
r.c.readErr = hideTempErr(err) c.readErr = hideTempErr(err)
if r.c.isServer { if c.isServer {
r.c.readMaskPos = maskBytes(r.c.readMaskKey, r.c.readMaskPos, b[:n]) c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n])
} }
r.c.readRemaining -= int64(n) c.readRemaining -= int64(n)
return n, r.c.readErr if c.readRemaining > 0 && c.readErr == io.EOF {
c.readErr = errUnexpectedEOF
}
return n, c.readErr
} }
if r.c.readFinal { if c.readFinal {
r.c.readSeq++ c.messageReader = nil
return 0, io.EOF return 0, io.EOF
} }
frameType, err := r.c.advanceFrame() frameType, err := c.advanceFrame()
switch { switch {
case err != nil: case err != nil:
r.c.readErr = hideTempErr(err) c.readErr = hideTempErr(err)
case frameType == TextMessage || frameType == BinaryMessage: case frameType == TextMessage || frameType == BinaryMessage:
r.c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader")
} }
} }
err := r.c.readErr err := c.readErr
if err == io.EOF && r.seq == r.c.readSeq { if err == io.EOF && c.messageReader == r {
err = errUnexpectedEOF err = errUnexpectedEOF
} }
return 0, err return 0, err
@@ -872,6 +941,11 @@ func (c *Conn) SetReadLimit(limit int64) {
c.readLimit = limit c.readLimit = limit
} }
// PingHandler returns the current ping handler
func (c *Conn) PingHandler() func(appData string) error {
return c.handlePing
}
// SetPingHandler sets the handler for ping messages received from the peer. // SetPingHandler sets the handler for ping messages received from the peer.
// The appData argument to h is the PING frame application data. The default // The appData argument to h is the PING frame application data. The default
// ping handler sends a pong to the peer. // ping handler sends a pong to the peer.
@@ -890,6 +964,11 @@ func (c *Conn) SetPingHandler(h func(appData string) error) {
c.handlePing = h c.handlePing = h
} }
// PongHandler returns the current pong handler
func (c *Conn) PongHandler() func(appData string) error {
return c.handlePong
}
// SetPongHandler sets the handler for pong messages received from the peer. // SetPongHandler sets the handler for pong messages received from the peer.
// The appData argument to h is the PONG frame application data. The default // The appData argument to h is the PONG frame application data. The default
// pong handler does nothing. // pong handler does nothing.
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
c.br.Discard(len(p))
return p, err
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
if len(p) > 0 {
// advance over the bytes just read
io.ReadFull(c.br, p)
}
return p, err
}
+35
View File
@@ -174,6 +174,41 @@ func TestCloseBeforeFinalFrame(t *testing.T) {
} }
} }
func TestEOFWithinFrame(t *testing.T) {
const bufSize = 64
for n := 0; ; n++ {
var b bytes.Buffer
wc := newConn(fakeNetConn{Reader: nil, Writer: &b}, false, 1024, 1024)
rc := newConn(fakeNetConn{Reader: &b, Writer: nil}, true, 1024, 1024)
w, _ := wc.NextWriter(BinaryMessage)
w.Write(make([]byte, bufSize))
w.Close()
if n >= b.Len() {
break
}
b.Truncate(n)
op, r, err := rc.NextReader()
if err == errUnexpectedEOF {
continue
}
if op != BinaryMessage || err != nil {
t.Fatalf("%d: NextReader() returned %d, %v", n, op, err)
}
_, err = io.Copy(ioutil.Discard, r)
if err != errUnexpectedEOF {
t.Fatalf("%d: io.Copy() returned %v, want %v", n, err, errUnexpectedEOF)
}
_, _, err = rc.NextReader()
if err != errUnexpectedEOF {
t.Fatalf("%d: NextReader() returned %v, want %v", n, err, errUnexpectedEOF)
}
}
}
func TestEOFBeforeFinalFrame(t *testing.T) { func TestEOFBeforeFinalFrame(t *testing.T) {
const bufSize = 512 const bufSize = 512
+12 -8
View File
@@ -85,18 +85,22 @@
// and pong. Call the connection WriteControl, WriteMessage or NextWriter // and pong. Call the connection WriteControl, WriteMessage or NextWriter
// methods to send a control message to the peer. // methods to send a control message to the peer.
// //
// Connections handle received ping and pong messages by invoking callback
// functions set with SetPingHandler and SetPongHandler methods. The default
// ping handler sends a pong to the client. The callback functions can be
// invoked from the NextReader, ReadMessage or the message Read method.
//
// Connections handle received close messages by sending a close message to the // Connections handle received close messages by sending a close message to the
// peer and returning a *CloseError from the the NextReader, ReadMessage or the // peer and returning a *CloseError from the the NextReader, ReadMessage or the
// message Read method. // message Read method.
// //
// The application must read the connection to process ping and close messages // Connections handle received ping and pong messages by invoking callback
// sent from the peer. If the application is not otherwise interested in // functions set with SetPingHandler and SetPongHandler methods. The callback
// messages from the peer, then the application should start a goroutine to // functions are called from the NextReader, ReadMessage and the message Read
// methods.
//
// The default ping handler sends a pong to the peer. The application's reading
// goroutine can block for a short time while the handler writes the pong data
// to the connection.
//
// The application must read the connection to process ping, pong and close
// messages sent from the peer. If the application is not otherwise interested
// in messages from the peer, then the application should start a goroutine to
// read and discard messages from the peer. A simple example is: // read and discard messages from the peer. A simple example is:
// //
// func readLoop(c *websocket.Conn) { // func readLoop(c *websocket.Conn) {
+7 -1
View File
@@ -12,6 +12,11 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
var (
c *websocket.Conn
req *http.Request
)
// The websocket.IsUnexpectedCloseError function is useful for identifying // The websocket.IsUnexpectedCloseError function is useful for identifying
// application and protocol errors. // application and protocol errors.
// //
@@ -20,7 +25,8 @@ import (
// only expected close message from the client has the code // only expected close message from the client has the code
// websocket.CloseGoingAway. All other other close messages are likely the // websocket.CloseGoingAway. All other other close messages are likely the
// result of an application or protocol error and are logged to aid debugging. // result of an application or protocol error and are logged to aid debugging.
func ExampleIsUnexpectedCloseError(err error, c *websocket.Conn, req *http.Request) { func ExampleIsUnexpectedCloseError() {
for { for {
messageType, p, err := c.ReadMessage() messageType, p, err := c.ReadMessage()
if err != nil { if err != nil {
+73 -2
View File
@@ -1,8 +1,8 @@
# Chat Example # Chat Example
This application shows how to use use the This application shows how to use use the
[websocket](https://github.com/gorilla/websocket) package and [websocket](https://github.com/gorilla/websocket) package to implement a simple
[jQuery](http://jquery.com) to implement a simple web chat application. web chat application.
## Running the example ## Running the example
@@ -18,3 +18,74 @@ using the following commands.
$ go run *.go $ go run *.go
To use the chat example, open http://localhost:8080/ in your browser. To use the chat example, open http://localhost:8080/ in your browser.
## Server
The server application defines two types, `Client` and `Hub`. The server
creates an instance of the `Client` type for each websocket connection. A
`Client` acts as an intermediary between the websocket connection and a single
instance of the `Hub` type. The `Hub` maintains a set of registered clients and
broadcasts messages to the clients.
The application runs one goroutine for the `Hub` and two goroutines for each
`Client`. The goroutines communicate with each other using channels. The `Hub`
has channels for registering clients, unregistering clients and broadcasting
messages. A `Client` has a buffered channel of outbound messages. One of the
client's goroutines reads messages from this channel and writes the messages to
the websocket. The other client goroutine reads messages from the websocket and
sends them to the hub.
### Hub
The code for the `Hub` type is in
[hub.go](https://github.com/gorilla/websocket/blob/master/examples/chat/hub.go).
The application's `main` function starts the hub's `run` method as a goroutine.
Clients send requests to the hub using the `register`, `unregister` and
`broadcast` channels.
The hub registers clients by adding the client pointer as a key in the
`clients` map. The map value is always true.
The unregister code is a little more complicated. In addition to deleting the
client pointer from the `clients` map, the hub closes the clients's `send`
channel to signal the client that no more messages will be sent to the client.
The hub handles messages by looping over the registered clients and sending the
message to the client's `send` channel. If the client's `send` buffer is full,
then the hub assumes that the client is dead or stuck. In this case, the hub
unregisters the client and closes the websocket.
### Client
The code for the `Client` type is in [client.go](https://github.com/gorilla/websocket/blob/master/examples/chat/client.go).
The `wsHandler` function is registered by the application's `main` function as
an HTTP handler. The handler upgrades the HTTP connection to the WebSocket
protocol, creates a client, registers the client with the hub and schedules the
client to be unregistered using a defer statement.
Next, the HTTP handler starts the client's `writePump` method as a goroutine.
This method transfers messages from the client's send channel to the websocket
connection. The writer method exits when the channel is closed by the hub or
there's an error writing to the websocket connection.
Finally, the HTTP handler calls the client's `readPump` method. This method
transfers inbound messages from the websocket to the hub.
## Frontend
The frontend code is in [home.html](https://github.com/gorilla/websocket/blob/master/examples/chat/home.html).
On document load, the script checks for websocket functionality in the browser.
If websocket functionality is available, then the script opens a connection to
the server and registers a callback to handle messages from the server. The
callback appends the message to the chat log using the appendLog function.
To allow the user to manually scroll through the chat log without interruption
from new messages, the `appendLog` function checks the scroll position before
adding new content. If the chat log is scrolled to the bottom, then the
function scrolls new content into view after adding the content. Otherwise, the
scroll position is not changed.
The form handler writes the user input to the websocket and clears the input
field.
@@ -5,10 +5,12 @@
package main package main
import ( import (
"github.com/gorilla/websocket" "bytes"
"log" "log"
"net/http" "net/http"
"time" "time"
"github.com/gorilla/websocket"
) )
const ( const (
@@ -25,62 +27,86 @@ const (
maxMessageSize = 512 maxMessageSize = 512
) )
var (
newline = []byte{'\n'}
space = []byte{' '}
)
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 1024, WriteBufferSize: 1024,
} }
// connection is an middleman between the websocket connection and the hub. // Client is an middleman between the websocket connection and the hub.
type connection struct { type Client struct {
hub *Hub
// The websocket connection. // The websocket connection.
ws *websocket.Conn conn *websocket.Conn
// Buffered channel of outbound messages. // Buffered channel of outbound messages.
send chan []byte send chan []byte
} }
// readPump pumps messages from the websocket connection to the hub. // readPump pumps messages from the websocket connection to the hub.
func (c *connection) readPump() { func (c *Client) readPump() {
defer func() { defer func() {
h.unregister <- c c.hub.unregister <- c
c.ws.Close() c.conn.Close()
}() }()
c.ws.SetReadLimit(maxMessageSize) c.conn.SetReadLimit(maxMessageSize)
c.ws.SetReadDeadline(time.Now().Add(pongWait)) c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for { for {
_, message, err := c.ws.ReadMessage() _, message, err := c.conn.ReadMessage()
if err != nil { if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
log.Printf("error: %v", err) log.Printf("error: %v", err)
} }
break break
} }
h.broadcast <- message message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
c.hub.broadcast <- message
} }
} }
// write writes a message with the given message type and payload. // write writes a message with the given message type and payload.
func (c *connection) write(mt int, payload []byte) error { func (c *Client) write(mt int, payload []byte) error {
c.ws.SetWriteDeadline(time.Now().Add(writeWait)) c.conn.SetWriteDeadline(time.Now().Add(writeWait))
return c.ws.WriteMessage(mt, payload) return c.conn.WriteMessage(mt, payload)
} }
// writePump pumps messages from the hub to the websocket connection. // writePump pumps messages from the hub to the websocket connection.
func (c *connection) writePump() { func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod) ticker := time.NewTicker(pingPeriod)
defer func() { defer func() {
ticker.Stop() ticker.Stop()
c.ws.Close() c.conn.Close()
}() }()
for { for {
select { select {
case message, ok := <-c.send: case message, ok := <-c.send:
if !ok { if !ok {
// The hub closed the channel.
c.write(websocket.CloseMessage, []byte{}) c.write(websocket.CloseMessage, []byte{})
return return
} }
if err := c.write(websocket.TextMessage, message); err != nil {
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
// Add queued chat messages to the current websocket message.
n := len(c.send)
for i := 0; i < n; i++ {
w.Write(newline)
w.Write(<-c.send)
}
if err := w.Close(); err != nil {
return return
} }
case <-ticker.C: case <-ticker.C:
@@ -92,14 +118,14 @@ func (c *connection) writePump() {
} }
// serveWs handles websocket requests from the peer. // serveWs handles websocket requests from the peer.
func serveWs(w http.ResponseWriter, r *http.Request) { func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return
} }
c := &connection{send: make(chan []byte, 256), ws: ws} client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
h.register <- c client.hub.register <- client
go c.writePump() go client.writePump()
c.readPump() client.readPump()
} }
+30 -24
View File
@@ -2,47 +2,53 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Chat Example</title> <title>Chat Example</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script type="text/javascript"> <script type="text/javascript">
$(function() { window.onload = function () {
var conn; var conn;
var msg = $("#msg"); var msg = document.getElementById("msg");
var log = $("#log"); var log = document.getElementById("log");
function appendLog(msg) { function appendLog(item) {
var d = log[0] var doScroll = log.scrollTop === log.scrollHeight - log.clientHeight;
var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight; log.appendChild(item);
msg.appendTo(log)
if (doScroll) { if (doScroll) {
d.scrollTop = d.scrollHeight - d.clientHeight; log.scrollTop = log.scrollHeight - log.clientHeight;
} }
} }
$("#form").submit(function() { document.getElementById("form").onsubmit = function () {
if (!conn) { if (!conn) {
return false; return false;
} }
if (!msg.val()) { if (!msg.value) {
return false; return false;
} }
conn.send(msg.val()); conn.send(msg.value);
msg.val(""); msg.value = "";
return false return false;
}); };
if (window["WebSocket"]) { if (window["WebSocket"]) {
conn = new WebSocket("ws://{{$}}/ws"); conn = new WebSocket("ws://{{$}}/ws");
conn.onclose = function(evt) { conn.onclose = function (evt) {
appendLog($("<div><b>Connection closed.</b></div>")) var item = document.createElement("div");
} item.innerHTML = "<b>Connection closed.</b>";
conn.onmessage = function(evt) { appendLog(item);
appendLog($("<div/>").text(evt.data)) };
} conn.onmessage = function (evt) {
var messages = evt.data.split('\n');
for (var i = 0; i < messages.length; i++) {
var item = document.createElement("div");
item.innerText = messages[i];
appendLog(item);
}
};
} else { } else {
appendLog($("<div><b>Your browser does not support WebSockets.</b></div>")) var item = document.createElement("div");
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
appendLog(item);
} }
}); };
</script> </script>
<style type="text/css"> <style type="text/css">
html { html {
+29 -27
View File
@@ -4,46 +4,48 @@
package main package main
// hub maintains the set of active connections and broadcasts messages to the // hub maintains the set of active clients and broadcasts messages to the
// connections. // clients.
type hub struct { type Hub struct {
// Registered connections. // Registered clients.
connections map[*connection]bool clients map[*Client]bool
// Inbound messages from the connections. // Inbound messages from the clients.
broadcast chan []byte broadcast chan []byte
// Register requests from the connections. // Register requests from the clients.
register chan *connection register chan *Client
// Unregister requests from connections. // Unregister requests from clients.
unregister chan *connection unregister chan *Client
} }
var h = hub{ func newHub() *Hub {
broadcast: make(chan []byte), return &Hub{
register: make(chan *connection), broadcast: make(chan []byte),
unregister: make(chan *connection), register: make(chan *Client),
connections: make(map[*connection]bool), unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
} }
func (h *hub) run() { func (h *Hub) run() {
for { for {
select { select {
case c := <-h.register: case client := <-h.register:
h.connections[c] = true h.clients[client] = true
case c := <-h.unregister: case client := <-h.unregister:
if _, ok := h.connections[c]; ok { if _, ok := h.clients[client]; ok {
delete(h.connections, c) delete(h.clients, client)
close(c.send) close(client.send)
} }
case m := <-h.broadcast: case message := <-h.broadcast:
for c := range h.connections { for client := range h.clients {
select { select {
case c.send <- m: case client.send <- message:
default: default:
close(c.send) close(client.send)
delete(h.connections, c) delete(h.clients, client)
} }
} }
} }
+8 -4
View File
@@ -12,9 +12,10 @@ import (
) )
var addr = flag.String("addr", ":8080", "http service address") var addr = flag.String("addr", ":8080", "http service address")
var homeTempl = template.Must(template.ParseFiles("home.html")) var homeTemplate = template.Must(template.ParseFiles("home.html"))
func serveHome(w http.ResponseWriter, r *http.Request) { func serveHome(w http.ResponseWriter, r *http.Request) {
log.Println(r.URL)
if r.URL.Path != "/" { if r.URL.Path != "/" {
http.Error(w, "Not found", 404) http.Error(w, "Not found", 404)
return return
@@ -24,14 +25,17 @@ func serveHome(w http.ResponseWriter, r *http.Request) {
return return
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
homeTempl.Execute(w, r.Host) homeTemplate.Execute(w, r.Host)
} }
func main() { func main() {
flag.Parse() flag.Parse()
go h.run() hub := newHub()
go hub.run()
http.HandleFunc("/", serveHome) http.HandleFunc("/", serveHome)
http.HandleFunc("/ws", serveWs) http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(hub, w, r)
})
err := http.ListenAndServe(*addr, nil) err := http.ListenAndServe(*addr, nil)
if err != nil { if err != nil {
log.Fatal("ListenAndServe: ", err) log.Fatal("ListenAndServe: ", err)
+1 -1
View File
@@ -120,7 +120,7 @@ func serveWs(w http.ResponseWriter, r *http.Request) {
} }
var lastMod time.Time var lastMod time.Time
if n, err := strconv.ParseInt(r.FormValue("lastMod"), 16, 64); err != nil { if n, err := strconv.ParseInt(r.FormValue("lastMod"), 16, 64); err == nil {
lastMod = time.Unix(0, n) lastMod = time.Unix(0, n)
} }
+9 -1
View File
@@ -53,6 +53,7 @@ func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status in
if u.Error != nil { if u.Error != nil {
u.Error(w, r, status, err) u.Error(w, r, status, err)
} else { } else {
w.Header().Set("Sec-Websocket-Version", "13")
http.Error(w, http.StatusText(status), status) http.Error(w, http.StatusText(status), status)
} }
return nil, err return nil, err
@@ -99,7 +100,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
if r.Method != "GET" { if r.Method != "GET" {
return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: method not GET") return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: method not GET")
} }
if values := r.Header["Sec-Websocket-Version"]; len(values) == 0 || values[0] != "13" { if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {
return u.returnError(w, r, http.StatusBadRequest, "websocket: version != 13") return u.returnError(w, r, http.StatusBadRequest, "websocket: version != 13")
} }
@@ -251,3 +252,10 @@ func Subprotocols(r *http.Request) []string {
} }
return protocols return protocols
} }
// IsWebSocketUpgrade returns true if the client requested upgrade to the
// WebSocket protocol.
func IsWebSocketUpgrade(r *http.Request) bool {
return tokenListContainsValue(r.Header, "Connection", "upgrade") &&
tokenListContainsValue(r.Header, "Upgrade", "websocket")
}
+18
View File
@@ -31,3 +31,21 @@ func TestSubprotocols(t *testing.T) {
} }
} }
} }
var isWebSocketUpgradeTests = []struct {
ok bool
h http.Header
}{
{false, http.Header{"Upgrade": {"websocket"}}},
{false, http.Header{"Connection": {"upgrade"}}},
{true, http.Header{"Connection": {"upgRade"}, "Upgrade": {"WebSocket"}}},
}
func TestIsWebSocketUpgrade(t *testing.T) {
for _, tt := range isWebSocketUpgradeTests {
ok := IsWebSocketUpgrade(&http.Request{Header: tt.h})
if tt.ok != ok {
t.Errorf("IsWebSocketUpgrade(%v) returned %v, want %v", tt.h, ok, tt.ok)
}
}
}
+183 -13
View File
@@ -13,19 +13,6 @@ import (
"strings" "strings"
) )
// tokenListContainsValue returns true if the 1#token header with the given
// name contains token.
func tokenListContainsValue(header http.Header, name string, value string) bool {
for _, v := range header[name] {
for _, s := range strings.Split(v, ",") {
if strings.EqualFold(value, strings.TrimSpace(s)) {
return true
}
}
}
return false
}
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
func computeAcceptKey(challengeKey string) string { func computeAcceptKey(challengeKey string) string {
@@ -42,3 +29,186 @@ func generateChallengeKey() (string, error) {
} }
return base64.StdEncoding.EncodeToString(p), nil return base64.StdEncoding.EncodeToString(p), nil
} }
// Octet types from RFC 2616.
var octetTypes [256]byte
const (
isTokenOctet = 1 << iota
isSpaceOctet
)
func init() {
// From RFC 2616
//
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
// CR = <US-ASCII CR, carriage return (13)>
// LF = <US-ASCII LF, linefeed (10)>
// SP = <US-ASCII SP, space (32)>
// HT = <US-ASCII HT, horizontal-tab (9)>
// <"> = <US-ASCII double-quote mark (34)>
// CRLF = CR LF
// LWS = [CRLF] 1*( SP | HT )
// TEXT = <any OCTET except CTLs, but including LWS>
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
// token = 1*<any CHAR except CTLs or separators>
// qdtext = <any TEXT except <">>
for c := 0; c < 256; c++ {
var t byte
isCtl := c <= 31 || c == 127
isChar := 0 <= c && c <= 127
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
t |= isSpaceOctet
}
if isChar && !isCtl && !isSeparator {
t |= isTokenOctet
}
octetTypes[c] = t
}
}
func skipSpace(s string) (rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isSpaceOctet == 0 {
break
}
}
return s[i:]
}
func nextToken(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isTokenOctet == 0 {
break
}
}
return s[:i], s[i:]
}
func nextTokenOrQuoted(s string) (value string, rest string) {
if !strings.HasPrefix(s, "\"") {
return nextToken(s)
}
s = s[1:]
for i := 0; i < len(s); i++ {
switch s[i] {
case '"':
return s[:i], s[i+1:]
case '\\':
p := make([]byte, len(s)-1)
j := copy(p, s[:i])
escape := true
for i = i + 1; i < len(s); i++ {
b := s[i]
switch {
case escape:
escape = false
p[j] = b
j += 1
case b == '\\':
escape = true
case b == '"':
return string(p[:j]), s[i+1:]
default:
p[j] = b
j += 1
}
}
return "", ""
}
}
return "", ""
}
// tokenListContainsValue returns true if the 1#token header with the given
// name contains token.
func tokenListContainsValue(header http.Header, name string, value string) bool {
headers:
for _, s := range header[name] {
for {
var t string
t, s = nextToken(skipSpace(s))
if t == "" {
continue headers
}
s = skipSpace(s)
if s != "" && s[0] != ',' {
continue headers
}
if strings.EqualFold(t, value) {
return true
}
if s == "" {
continue headers
}
s = s[1:]
}
}
return false
}
// parseExtensiosn parses WebSocket extensions from a header.
func parseExtensions(header http.Header) []map[string]string {
// From RFC 6455:
//
// Sec-WebSocket-Extensions = extension-list
// extension-list = 1#extension
// extension = extension-token *( ";" extension-param )
// extension-token = registered-token
// registered-token = token
// extension-param = token [ "=" (token | quoted-string) ]
// ;When using the quoted-string syntax variant, the value
// ;after quoted-string unescaping MUST conform to the
// ;'token' ABNF.
var result []map[string]string
headers:
for _, s := range header["Sec-Websocket-Extensions"] {
for {
var t string
t, s = nextToken(skipSpace(s))
if t == "" {
continue headers
}
ext := map[string]string{"": t}
for {
s = skipSpace(s)
if !strings.HasPrefix(s, ";") {
break
}
var k string
k, s = nextToken(skipSpace(s[1:]))
if k == "" {
continue headers
}
s = skipSpace(s)
var v string
if strings.HasPrefix(s, "=") {
v, s = nextTokenOrQuoted(skipSpace(s[1:]))
s = skipSpace(s)
}
if s != "" && s[0] != ',' && s[0] != ';' {
continue headers
}
ext[k] = v
}
if s != "" && s[0] != ',' {
continue headers
}
result = append(result, ext)
if s == "" {
continue headers
}
s = s[1:]
}
}
return result
}
+40
View File
@@ -6,6 +6,7 @@ package websocket
import ( import (
"net/http" "net/http"
"reflect"
"testing" "testing"
) )
@@ -32,3 +33,42 @@ func TestTokenListContainsValue(t *testing.T) {
} }
} }
} }
var parseExtensionTests = []struct {
value string
extensions []map[string]string
}{
{`foo`, []map[string]string{map[string]string{"": "foo"}}},
{`foo, bar; baz=2`, []map[string]string{
map[string]string{"": "foo"},
map[string]string{"": "bar", "baz": "2"}}},
{`foo; bar="b,a;z"`, []map[string]string{
map[string]string{"": "foo", "bar": "b,a;z"}}},
{`foo , bar; baz = 2`, []map[string]string{
map[string]string{"": "foo"},
map[string]string{"": "bar", "baz": "2"}}},
{`foo, bar; baz=2 junk`, []map[string]string{
map[string]string{"": "foo"}}},
{`foo junk, bar; baz=2 junk`, nil},
{`mux; max-channels=4; flow-control, deflate-stream`, []map[string]string{
map[string]string{"": "mux", "max-channels": "4", "flow-control": ""},
map[string]string{"": "deflate-stream"}}},
{`permessage-foo; x="10"`, []map[string]string{
map[string]string{"": "permessage-foo", "x": "10"}}},
{`permessage-foo; use_y, permessage-foo`, []map[string]string{
map[string]string{"": "permessage-foo", "use_y": ""},
map[string]string{"": "permessage-foo"}}},
{`permessage-deflate; client_max_window_bits; server_max_window_bits=10 , permessage-deflate; client_max_window_bits`, []map[string]string{
map[string]string{"": "permessage-deflate", "client_max_window_bits": "", "server_max_window_bits": "10"},
map[string]string{"": "permessage-deflate", "client_max_window_bits": ""}}},
}
func TestParseExtensions(t *testing.T) {
for _, tt := range parseExtensionTests {
h := http.Header{http.CanonicalHeaderKey("Sec-WebSocket-Extensions"): {tt.value}}
extensions := parseExtensions(h)
if !reflect.DeepEqual(extensions, tt.extensions) {
t.Errorf("parseExtensions(%q)\n = %v,\nwant %v", tt.value, extensions, tt.extensions)
}
}
}