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/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 src="test.pusher.js"></script>
+5
View File
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/pusher/pusher-http-go"
@@ -20,6 +21,7 @@ func init() {
}
func pusherPresenceAuth(res http.ResponseWriter, req *http.Request) {
log.Println("Presence Request")
presenceData := pusher.MemberData{
UserId: "1",
UserInfo: map[string]string{},
@@ -39,6 +41,9 @@ func pusherPrivateAuth(res http.ResponseWriter, req *http.Request) {
params, _ := ioutil.ReadAll(req.Body)
response, err := client.AuthenticatePrivateChannel(params)
log.Printf("Private Request %s", params)
log.Printf("Auth %s", response)
if err != nil {
panic(err)
}
Generated
+6 -6
View File
@@ -1,14 +1,14 @@
hash: bfb508bdf4f85c71c7eef4bb9431148ee373dfaf31d001d050d933f80ac4288e
updated: 2016-02-21T20:13:57.03748112-03:00
updated: 2016-08-09T22:49:29.988191345-03:00
imports:
- name: github.com/golang/glog
version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998
- name: github.com/gorilla/context
version: 1c83b3eabd45b6d76072b66b746c20815fb2872d
version: aed02d124ae4a0e94fea4541c8effd05bf0c8296
- name: github.com/gorilla/mux
version: 26a6070f849969ba72b72256e9f14cf519751690
version: 780415097119f6f61c55475fe59b66f3c3e9ea53
- name: github.com/gorilla/websocket
version: 5c91b59efa232fa9a87b705d54101832c498a172
version: a69d25be2fe2923a97c2af6849b2f52426f68fc0
- name: github.com/pusher/pusher-http-go
version: 8d4ffe157699620440932e4d03253a22533f2e43
devImports: []
version: 2bba5f217f6f0f4f0c0a9bb11b945b206b32bec5
testImports: []
+15 -5
View File
@@ -1,8 +1,18 @@
language: go
sudo: false
go:
- 1.3
- 1.4
- 1.5
- tip
matrix:
include:
- go: 1.3
- go: 1.4
- 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
values, ok = GetAllOk(emptyR)
assertEqual(value, nil)
assertEqual(len(values), 0)
assertEqual(ok, false)
// Delete()
+13 -7
View File
@@ -1,14 +1,20 @@
language: go
sudo: false
go:
- 1.3
- 1.4
- 1.5
- tip
matrix:
include:
- go: 1.2
- go: 1.3
- go: 1.4
- go: 1.5
- go: 1.6
- go: tip
install:
- go get golang.org/x/tools/cmd/vet
- # Skip
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d -s .)
- diff -u <(echo -n) <(gofmt -d .)
- go tool vet .
- 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)
[![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:
* 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.
* 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.
* 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:
@@ -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.
### 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:
```go
@@ -116,7 +144,7 @@ Then register routes in the subrouter:
```go
s.HandleFunc("/products/", ProductsHandler)
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.
@@ -136,6 +164,37 @@ s.HandleFunc("/{key}/", ProductHandler)
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.
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 (
"net/http"
"log"
"github.com/gorilla/mux"
)
@@ -231,7 +290,7 @@ func main() {
r.HandleFunc("/", YourHandler)
// 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 (
"net/http"
"net/http/httptest"
"testing"
)
@@ -19,3 +20,30 @@ func BenchmarkMux(b *testing.B) {
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.
/*
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
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"
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.
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"
"path"
"regexp"
"github.com/gorilla/context"
)
// NewRouter returns a new router instance.
@@ -48,7 +46,11 @@ type Router struct {
namedRoutes map[string]*Route
// See Router.StrictSlash(). This defines the flag for new routes.
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
}
@@ -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
// mux.Vars(request).
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Clean path to canonical form and redirect.
if p := cleanPath(req.URL.Path); p != req.URL.Path {
if !r.skipClean {
// 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.
// 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
url := *req.URL
url.Path = p
p = url.String()
// 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:
// http://code.google.com/p/go/issues/detail?id=5252
url := *req.URL
url.Path = p
p = url.String()
w.Header().Set("Location", p)
w.WriteHeader(http.StatusMovedPermanently)
return
w.Header().Set("Location", p)
w.WriteHeader(http.StatusMovedPermanently)
return
}
}
var match RouteMatch
var handler http.Handler
if r.Match(req, &match) {
handler = match.Handler
setVars(req, match.Vars)
setCurrentRoute(req, match.Route)
req = setVars(req, match.Vars)
req = setCurrentRoute(req, match.Route)
}
if handler == nil {
handler = http.NotFoundHandler()
}
if !r.KeepContext {
defer context.Clear(req)
defer contextClear(req)
}
handler.ServeHTTP(w, req)
}
@@ -133,6 +137,19 @@ func (r *Router) StrictSlash(value bool) *Router {
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
// ----------------------------------------------------------------------------
@@ -170,7 +187,7 @@ func (r *Router) buildVars(m map[string]string) map[string]string {
// NewRoute registers an empty 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)
return route
}
@@ -236,7 +253,7 @@ func (r *Router) Schemes(schemes ...string) *Route {
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.
func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route {
return r.NewRoute().BuildVarsFunc(f)
@@ -268,6 +285,9 @@ func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error {
if err == SkipRouter {
continue
}
if err != nil {
return err
}
for _, sr := range t.matchers {
if h, ok := sr.(*Router); ok {
err := h.walk(walkFn, ancestors)
@@ -308,7 +328,7 @@ const (
// Vars returns the route variables for the current request, if any.
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 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
// Router.
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 nil
}
func setVars(r *http.Request, val interface{}) {
if val != nil {
context.Set(r, varsKey, val)
}
func setVars(r *http.Request, val interface{}) *http.Request {
return contextSet(r, varsKey, val)
}
func setCurrentRoute(r *http.Request, val interface{}) {
if val != nil {
context.Set(r, routeKey, val)
}
func setCurrentRoute(r *http.Request, val interface{}) *http.Request {
return contextSet(r, routeKey, val)
}
// ----------------------------------------------------------------------------
@@ -357,6 +373,7 @@ func cleanPath(p string) string {
if p[len(p)-1] == '/' && np != "/" {
np += "/"
}
return np
}
+420 -302
View File
@@ -5,12 +5,11 @@
package mux
import (
"errors"
"fmt"
"net/http"
"strings"
"testing"
"github.com/gorilla/context"
)
func (r *Route) GoString() string {
@@ -32,6 +31,8 @@ type routeTest struct {
vars map[string]string // the expected vars of the match
host string // the expected host 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
shouldRedirect bool // whether the request should result in a redirect
}
@@ -113,116 +114,129 @@ func TestHost(t *testing.T) {
shouldMatch: false,
},
{
title: "Host route with pattern, match",
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb"},
host: "aaa.bbb.ccc",
path: "",
shouldMatch: true,
title: "Host route with pattern, match",
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb"},
host: "aaa.bbb.ccc",
path: "",
hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`,
shouldMatch: true,
},
{
title: "Host route with pattern, additional capturing group, match",
route: new(Route).Host("aaa.{v1:[a-z]{2}(b|c)}.ccc"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb"},
host: "aaa.bbb.ccc",
path: "",
shouldMatch: true,
title: "Host route with pattern, additional capturing group, match",
route: new(Route).Host("aaa.{v1:[a-z]{2}(b|c)}.ccc"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb"},
host: "aaa.bbb.ccc",
path: "",
hostTemplate: `aaa.{v1:[a-z]{2}(b|c)}.ccc`,
shouldMatch: true,
},
{
title: "Host route with pattern, wrong host in request URL",
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb"},
host: "aaa.bbb.ccc",
path: "",
shouldMatch: false,
title: "Host route with pattern, wrong host in request URL",
route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb"},
host: "aaa.bbb.ccc",
path: "",
hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`,
shouldMatch: false,
},
{
title: "Host route with multiple patterns, match",
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"),
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"},
host: "aaa.bbb.ccc",
path: "",
shouldMatch: true,
title: "Host route with multiple patterns, match",
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"),
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"},
host: "aaa.bbb.ccc",
path: "",
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",
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"),
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"},
host: "aaa.bbb.ccc",
path: "",
shouldMatch: false,
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}}"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"},
host: "aaa.bbb.ccc",
path: "",
hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`,
shouldMatch: false,
},
{
title: "Host route with hyphenated name and pattern, match",
route: new(Route).Host("aaa.{v-1:[a-z]{3}}.ccc"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v-1": "bbb"},
host: "aaa.bbb.ccc",
path: "",
shouldMatch: true,
title: "Host route with hyphenated name and pattern, match",
route: new(Route).Host("aaa.{v-1:[a-z]{3}}.ccc"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v-1": "bbb"},
host: "aaa.bbb.ccc",
path: "",
hostTemplate: `aaa.{v-1:[a-z]{3}}.ccc`,
shouldMatch: true,
},
{
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"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v-1": "bbb"},
host: "aaa.bbb.ccc",
path: "",
shouldMatch: true,
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"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v-1": "bbb"},
host: "aaa.bbb.ccc",
path: "",
hostTemplate: `aaa.{v-1:[a-z]{2}(b|c)}.ccc`,
shouldMatch: true,
},
{
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}}"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v-1": "aaa", "v-2": "bbb", "v-3": "ccc"},
host: "aaa.bbb.ccc",
path: "",
shouldMatch: true,
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}}"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v-1": "aaa", "v-2": "bbb", "v-3": "ccc"},
host: "aaa.bbb.ccc",
path: "",
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",
route: new(Route).Path("/{category:a|b/c}"),
request: newRequest("GET", "http://localhost/a"),
vars: map[string]string{"category": "a"},
host: "",
path: "/a",
shouldMatch: true,
title: "Path route with single pattern with pipe, match",
route: new(Route).Path("/{category:a|b/c}"),
request: newRequest("GET", "http://localhost/a"),
vars: map[string]string{"category": "a"},
host: "",
path: "/a",
pathTemplate: `/{category:a|b/c}`,
shouldMatch: true,
},
{
title: "Path route with single pattern with pipe, match",
route: new(Route).Path("/{category:a|b/c}"),
request: newRequest("GET", "http://localhost/b/c"),
vars: map[string]string{"category": "b/c"},
host: "",
path: "/b/c",
shouldMatch: true,
title: "Path route with single pattern with pipe, match",
route: new(Route).Path("/{category:a|b/c}"),
request: newRequest("GET", "http://localhost/b/c"),
vars: map[string]string{"category": "b/c"},
host: "",
path: "/b/c",
pathTemplate: `/{category:a|b/c}`,
shouldMatch: true,
},
{
title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/a/product_name/1"),
vars: map[string]string{"category": "a", "product": "product_name", "id": "1"},
host: "",
path: "/a/product_name/1",
shouldMatch: true,
title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/a/product_name/1"),
vars: map[string]string{"category": "a", "product": "product_name", "id": "1"},
host: "",
path: "/a/product_name/1",
pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`,
shouldMatch: true,
},
{
title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/b/c/product_name/1"),
vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"},
host: "",
path: "/b/c/product_name/1",
shouldMatch: true,
title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/b/c/product_name/1"),
vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"},
host: "",
path: "/b/c/product_name/1",
pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`,
shouldMatch: true,
},
}
for _, test := range tests {
testRoute(t, test)
testTemplate(t, test)
}
}
@@ -247,22 +261,24 @@ func TestPath(t *testing.T) {
shouldMatch: true,
},
{
title: "Path route, do not match with trailing slash in path",
route: new(Route).Path("/111/"),
request: newRequest("GET", "http://localhost/111"),
vars: map[string]string{},
host: "",
path: "/111",
shouldMatch: false,
title: "Path route, do not match with trailing slash in path",
route: new(Route).Path("/111/"),
request: newRequest("GET", "http://localhost/111"),
vars: map[string]string{},
host: "",
path: "/111",
pathTemplate: `/111/`,
shouldMatch: false,
},
{
title: "Path route, do not match with trailing slash in request",
route: new(Route).Path("/111"),
request: newRequest("GET", "http://localhost/111/"),
vars: map[string]string{},
host: "",
path: "/111/",
shouldMatch: false,
title: "Path route, do not match with trailing slash in request",
route: new(Route).Path("/111"),
request: newRequest("GET", "http://localhost/111/"),
vars: map[string]string{},
host: "",
path: "/111/",
pathTemplate: `/111`,
shouldMatch: false,
},
{
title: "Path route, wrong path in request in request URL",
@@ -274,81 +290,100 @@ func TestPath(t *testing.T) {
shouldMatch: false,
},
{
title: "Path route with pattern, match",
route: new(Route).Path("/111/{v1:[0-9]{3}}/333"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "222"},
host: "",
path: "/111/222/333",
shouldMatch: true,
title: "Path route with pattern, match",
route: new(Route).Path("/111/{v1:[0-9]{3}}/333"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "222"},
host: "",
path: "/111/222/333",
pathTemplate: `/111/{v1:[0-9]{3}}/333`,
shouldMatch: true,
},
{
title: "Path route with pattern, URL in request does not match",
route: new(Route).Path("/111/{v1:[0-9]{3}}/333"),
request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "222"},
host: "",
path: "/111/222/333",
shouldMatch: false,
title: "Path route with pattern, URL in request does not match",
route: new(Route).Path("/111/{v1:[0-9]{3}}/333"),
request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "222"},
host: "",
path: "/111/222/333",
pathTemplate: `/111/{v1:[0-9]{3}}/333`,
shouldMatch: false,
},
{
title: "Path route with multiple patterns, match",
route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"},
host: "",
path: "/111/222/333",
shouldMatch: true,
title: "Path route with multiple patterns, match",
route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"},
host: "",
path: "/111/222/333",
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",
route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"},
host: "",
path: "/111/222/333",
shouldMatch: false,
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}}"),
request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"},
host: "",
path: "/111/222/333",
pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`,
shouldMatch: false,
},
{
title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|(b/c)}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/a/product_name/1"),
vars: map[string]string{"category": "a", "product": "product_name", "id": "1"},
host: "",
path: "/a/product_name/1",
shouldMatch: true,
title: "Path route with multiple patterns with pipe, match",
route: new(Route).Path("/{category:a|(b/c)}/{product}/{id:[0-9]+}"),
request: newRequest("GET", "http://localhost/a/product_name/1"),
vars: map[string]string{"category": "a", "product": "product_name", "id": "1"},
host: "",
path: "/a/product_name/1",
pathTemplate: `/{category:a|(b/c)}/{product}/{id:[0-9]+}`,
shouldMatch: true,
},
{
title: "Path route with hyphenated name and pattern, match",
route: new(Route).Path("/111/{v-1:[0-9]{3}}/333"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v-1": "222"},
host: "",
path: "/111/222/333",
shouldMatch: true,
title: "Path route with hyphenated name and pattern, match",
route: new(Route).Path("/111/{v-1:[0-9]{3}}/333"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v-1": "222"},
host: "",
path: "/111/222/333",
pathTemplate: `/111/{v-1:[0-9]{3}}/333`,
shouldMatch: true,
},
{
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}}"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v-1": "111", "v-2": "222", "v-3": "333"},
host: "",
path: "/111/222/333",
shouldMatch: true,
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}}"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v-1": "111", "v-2": "222", "v-3": "333"},
host: "",
path: "/111/222/333",
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",
route: new(Route).Path("/{product-category:a|(b/c)}/{product-name}/{product-id:[0-9]+}"),
request: newRequest("GET", "http://localhost/a/product_name/1"),
vars: map[string]string{"product-category": "a", "product-name": "product_name", "product-id": "1"},
host: "",
path: "/a/product_name/1",
shouldMatch: true,
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]+}"),
request: newRequest("GET", "http://localhost/a/product_name/1"),
vars: map[string]string{"product-category": "a", "product-name": "product_name", "product-id": "1"},
host: "",
path: "/a/product_name/1",
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 {
testRoute(t, test)
testTemplate(t, test)
}
}
@@ -382,108 +417,126 @@ func TestPathPrefix(t *testing.T) {
shouldMatch: false,
},
{
title: "PathPrefix route with pattern, match",
route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "222"},
host: "",
path: "/111/222",
shouldMatch: true,
title: "PathPrefix route with pattern, match",
route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "222"},
host: "",
path: "/111/222",
pathTemplate: `/111/{v1:[0-9]{3}}`,
shouldMatch: true,
},
{
title: "PathPrefix route with pattern, URL prefix in request does not match",
route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "222"},
host: "",
path: "/111/222",
shouldMatch: false,
title: "PathPrefix route with pattern, URL prefix in request does not match",
route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "222"},
host: "",
path: "/111/222",
pathTemplate: `/111/{v1:[0-9]{3}}`,
shouldMatch: false,
},
{
title: "PathPrefix route with multiple patterns, match",
route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "111", "v2": "222"},
host: "",
path: "/111/222",
shouldMatch: true,
title: "PathPrefix route with multiple patterns, match",
route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"),
request: newRequest("GET", "http://localhost/111/222/333"),
vars: map[string]string{"v1": "111", "v2": "222"},
host: "",
path: "/111/222",
pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`,
shouldMatch: true,
},
{
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}}"),
request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "111", "v2": "222"},
host: "",
path: "/111/222",
shouldMatch: false,
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}}"),
request: newRequest("GET", "http://localhost/111/aaa/333"),
vars: map[string]string{"v1": "111", "v2": "222"},
host: "",
path: "/111/222",
pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`,
shouldMatch: false,
},
}
for _, test := range tests {
testRoute(t, test)
testTemplate(t, test)
}
}
func TestHostPath(t *testing.T) {
tests := []routeTest{
{
title: "Host and Path route, match",
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{},
host: "",
path: "",
shouldMatch: true,
title: "Host and Path route, match",
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{},
host: "",
path: "",
pathTemplate: `/111/222/333`,
hostTemplate: `aaa.bbb.ccc`,
shouldMatch: true,
},
{
title: "Host and Path route, wrong host in request URL",
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{},
host: "",
path: "",
shouldMatch: false,
title: "Host and Path route, wrong host in request URL",
route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{},
host: "",
path: "",
pathTemplate: `/111/222/333`,
hostTemplate: `aaa.bbb.ccc`,
shouldMatch: false,
},
{
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"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb", "v2": "222"},
host: "aaa.bbb.ccc",
path: "/111/222/333",
shouldMatch: true,
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"),
request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb", "v2": "222"},
host: "aaa.bbb.ccc",
path: "/111/222/333",
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",
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"),
vars: map[string]string{"v1": "bbb", "v2": "222"},
host: "aaa.bbb.ccc",
path: "/111/222/333",
shouldMatch: false,
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"),
request: newRequest("GET", "http://aaa.222.ccc/111/222/333"),
vars: map[string]string{"v1": "bbb", "v2": "222"},
host: "aaa.bbb.ccc",
path: "/111/222/333",
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",
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"),
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"},
host: "aaa.bbb.ccc",
path: "/111/222/333",
shouldMatch: true,
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}}"),
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"},
host: "aaa.bbb.ccc",
path: "/111/222/333",
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",
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"),
vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"},
host: "aaa.bbb.ccc",
path: "/111/222/333",
shouldMatch: false,
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}}"),
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"},
host: "aaa.bbb.ccc",
path: "/111/222/333",
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 {
testRoute(t, test)
testTemplate(t, test)
}
}
@@ -541,6 +594,7 @@ func TestHeaders(t *testing.T) {
for _, test := range tests {
testRoute(t, test)
testTemplate(t, test)
}
}
@@ -578,6 +632,7 @@ func TestMethods(t *testing.T) {
for _, test := range tests {
testRoute(t, test)
testTemplate(t, test)
}
}
@@ -593,22 +648,26 @@ func TestQueries(t *testing.T) {
shouldMatch: true,
},
{
title: "Queries route, match with a query string",
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"),
vars: map[string]string{},
host: "",
path: "",
shouldMatch: true,
title: "Queries route, match with a query string",
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"),
vars: map[string]string{},
host: "",
path: "",
pathTemplate: `/api`,
hostTemplate: `www.example.com`,
shouldMatch: true,
},
{
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"),
request: newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"),
vars: map[string]string{},
host: "",
path: "",
shouldMatch: true,
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"),
request: newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"),
vars: map[string]string{},
host: "",
path: "",
pathTemplate: `/api`,
hostTemplate: `www.example.com`,
shouldMatch: true,
},
{
title: "Queries route, bad query",
@@ -803,6 +862,7 @@ func TestQueries(t *testing.T) {
for _, test := range tests {
testRoute(t, test)
testTemplate(t, test)
}
}
@@ -839,6 +899,7 @@ func TestSchemes(t *testing.T) {
}
for _, test := range tests {
testRoute(t, test)
testTemplate(t, test)
}
}
@@ -873,6 +934,7 @@ func TestMatcherFunc(t *testing.T) {
for _, test := range tests {
testRoute(t, test)
testTemplate(t, test)
}
}
@@ -885,9 +947,10 @@ func TestBuildVarsFunc(t *testing.T) {
vars["v2"] = "a"
return vars
}),
request: newRequest("GET", "http://localhost/111/2"),
path: "/111/3a",
shouldMatch: true,
request: newRequest("GET", "http://localhost/111/2"),
path: "/111/3a",
pathTemplate: `/111/{v1:\d}{v2:.*}`,
shouldMatch: true,
},
{
title: "BuildVarsFunc set on route and parent route",
@@ -898,14 +961,16 @@ func TestBuildVarsFunc(t *testing.T) {
vars["v2"] = "b"
return vars
}),
request: newRequest("GET", "http://localhost/1/a"),
path: "/2/b",
shouldMatch: true,
request: newRequest("GET", "http://localhost/1/a"),
path: "/2/b",
pathTemplate: `/{v1:\d}/{v2:\w}`,
shouldMatch: true,
},
}
for _, test := range tests {
testRoute(t, test)
testTemplate(t, test)
}
}
@@ -915,41 +980,48 @@ func TestSubRouter(t *testing.T) {
tests := []routeTest{
{
route: subrouter1.Path("/{v2:[a-z]+}"),
request: newRequest("GET", "http://aaa.google.com/bbb"),
vars: map[string]string{"v1": "aaa", "v2": "bbb"},
host: "aaa.google.com",
path: "/bbb",
shouldMatch: true,
route: subrouter1.Path("/{v2:[a-z]+}"),
request: newRequest("GET", "http://aaa.google.com/bbb"),
vars: map[string]string{"v1": "aaa", "v2": "bbb"},
host: "aaa.google.com",
path: "/bbb",
pathTemplate: `/{v2:[a-z]+}`,
hostTemplate: `{v1:[a-z]+}.google.com`,
shouldMatch: true,
},
{
route: subrouter1.Path("/{v2:[a-z]+}"),
request: newRequest("GET", "http://111.google.com/111"),
vars: map[string]string{"v1": "aaa", "v2": "bbb"},
host: "aaa.google.com",
path: "/bbb",
shouldMatch: false,
route: subrouter1.Path("/{v2:[a-z]+}"),
request: newRequest("GET", "http://111.google.com/111"),
vars: map[string]string{"v1": "aaa", "v2": "bbb"},
host: "aaa.google.com",
path: "/bbb",
pathTemplate: `/{v2:[a-z]+}`,
hostTemplate: `{v1:[a-z]+}.google.com`,
shouldMatch: false,
},
{
route: subrouter2.Path("/baz/{v2}"),
request: newRequest("GET", "http://localhost/foo/bar/baz/ding"),
vars: map[string]string{"v1": "bar", "v2": "ding"},
host: "",
path: "/foo/bar/baz/ding",
shouldMatch: true,
route: subrouter2.Path("/baz/{v2}"),
request: newRequest("GET", "http://localhost/foo/bar/baz/ding"),
vars: map[string]string{"v1": "bar", "v2": "ding"},
host: "",
path: "/foo/bar/baz/ding",
pathTemplate: `/foo/{v1}/baz/{v2}`,
shouldMatch: true,
},
{
route: subrouter2.Path("/baz/{v2}"),
request: newRequest("GET", "http://localhost/foo/bar"),
vars: map[string]string{"v1": "bar", "v2": "ding"},
host: "",
path: "/foo/bar/baz/ding",
shouldMatch: false,
route: subrouter2.Path("/baz/{v2}"),
request: newRequest("GET", "http://localhost/foo/bar"),
vars: map[string]string{"v1": "bar", "v2": "ding"},
host: "",
path: "/foo/bar/baz/ding",
pathTemplate: `/foo/{v1}/baz/{v2}`,
shouldMatch: false,
},
}
for _, test := range tests {
testRoute(t, test)
testTemplate(t, test)
}
}
@@ -1045,6 +1117,7 @@ func TestStrictSlash(t *testing.T) {
for _, test := range tests {
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) {
superRouterCalled := false
subRouterCalled := false
@@ -1152,14 +1261,13 @@ func TestSubrouterErrorHandling(t *testing.T) {
// ----------------------------------------------------------------------------
func getRouteTemplate(route *Route) string {
host, path := "none", "none"
if route.regexp != nil {
if route.regexp.host != nil {
host = route.regexp.host.template
}
if route.regexp.path != nil {
path = route.regexp.path.template
}
host, err := route.GetHostTemplate()
if err != nil {
host = "none"
}
path, err := route.GetPathTemplate()
if err != nil {
path = "none"
}
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
// 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")
func testTemplate(t *testing.T, test routeTest) {
route := test.route
pathTemplate := test.pathTemplate
if len(pathTemplate) == 0 {
pathTemplate = test.path
}
hostTemplate := test.hostTemplate
if len(hostTemplate) == 0 {
hostTemplate = test.host
}
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")
routePathTemplate, pathErr := route.GetPathTemplate()
if pathErr == nil && routePathTemplate != pathTemplate {
t.Errorf("(%v) GetPathTemplate not equal: expected %v, got %v", test.title, pathTemplate, routePathTemplate)
}
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 {
@@ -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
func TestSubrouterHeader(t *testing.T) {
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.
func (rw *ResponseRecorder) Header() http.Header {
return rw.HeaderMap
@@ -576,10 +572,10 @@ func TestSubRouting(t *testing.T) {
}
u, _ := router.Get("products").URL()
builtUrl := u.String()
builtURL := u.String()
// Yay, subroute aware of the domain when building!
if builtUrl != url {
t.Errorf("Expected %q, got %q.", url, builtUrl)
if builtURL != url {
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])
}
// Build the regexp pattern.
varIdx := i / 2
fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(varIdx), patt)
fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt)
// Build the reverse template.
fmt.Fprintf(reverse, "%s%%s", raw)
// Append variable name and compiled pattern.
varsN[varIdx] = name
varsR[varIdx], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
varsN[i/2] = name
varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt))
if err != nil {
return nil, err
}
@@ -148,10 +148,11 @@ func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
if !r.matchHost {
if r.matchQuery {
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))
}
@@ -181,10 +182,10 @@ func (r *routeRegexp) url(values map[string]string) (string, error) {
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
// value pair for the routeRegexp.
func (r *routeRegexp) getUrlQuery(req *http.Request) string {
func (r *routeRegexp) getURLQuery(req *http.Request) string {
if !r.matchQuery {
return ""
}
@@ -198,14 +199,14 @@ func (r *routeRegexp) getUrlQuery(req *http.Request) string {
}
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.
// It returns an error in case of unbalanced braces.
func braceIndices(s string) ([]int, error) {
var level, idx int
idxs := make([]int, 0)
var idxs []int
for i := 0; i < len(s); i++ {
switch s[i] {
case '{':
@@ -246,30 +247,17 @@ type routeRegexpGroup struct {
func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) {
// Store host variables.
if v.host != nil {
hostVars := v.host.regexp.FindStringSubmatch(getHost(req))
if hostVars != nil {
subexpNames := v.host.regexp.SubexpNames()
varName := 0
for i, name := range subexpNames[1:] {
if name != "" && name == varGroupName(varName) {
m.Vars[v.host.varsN[varName]] = hostVars[i+1]
varName++
}
}
host := getHost(req)
matches := v.host.regexp.FindStringSubmatchIndex(host)
if len(matches) > 0 {
extractVars(host, matches, v.host.varsN, m.Vars)
}
}
// Store path variables.
if v.path != nil {
pathVars := v.path.regexp.FindStringSubmatch(req.URL.Path)
if pathVars != nil {
subexpNames := v.path.regexp.SubexpNames()
varName := 0
for i, name := range subexpNames[1:] {
if name != "" && name == varGroupName(varName) {
m.Vars[v.path.varsN[varName]] = pathVars[i+1]
varName++
}
}
matches := v.path.regexp.FindStringSubmatchIndex(req.URL.Path)
if len(matches) > 0 {
extractVars(req.URL.Path, matches, v.path.varsN, m.Vars)
// Check if we should redirect.
if v.path.strictSlash {
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.
for _, q := range v.queries {
queryVars := q.regexp.FindStringSubmatch(q.getUrlQuery(req))
if queryVars != nil {
subexpNames := q.regexp.SubexpNames()
varName := 0
for i, name := range subexpNames[1:] {
if name != "" && name == varGroupName(varName) {
m.Vars[q.varsN[varName]] = queryVars[i+1]
varName++
}
}
queryURL := q.getURLQuery(req)
matches := q.regexp.FindStringSubmatchIndex(queryURL)
if len(matches) > 0 {
extractVars(queryURL, matches, q.varsN, m.Vars)
}
}
}
@@ -315,3 +297,16 @@ func getHost(r *http.Request) string {
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
// redirect to the former and vice versa.
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.
buildOnly bool
// The name used to build URLs.
@@ -36,6 +39,10 @@ type Route struct {
buildVarsFunc BuildVarsFunc
}
func (r *Route) SkipClean() bool {
return r.skipClean
}
// Match matches the route against the request.
func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
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)
}
// Regular expressions can be used with headers as well.
// It accepts a sequence of key/value pairs, where the value has regex support. For example
// HeadersRegexp accepts a sequence of key/value pairs, where the value has regex
// support. For example:
//
// r := mux.NewRouter()
// r.HeadersRegexp("Content-Type", "application/(text|json)",
// "X-Requested-With", "XMLHttpRequest")
@@ -263,6 +271,7 @@ func (r *Route) Host(tpl string) *Route {
// MatcherFunc is the function signature used by custom matchers.
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 {
return m(r, match)
}
@@ -532,6 +541,36 @@ func (r *Route) URLPath(pairs ...string) (*url.URL, error) {
}, 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
// BuildVarsFunc, it is invoked.
func (r *Route) prepareVars(pairs ...string) (map[string]string, error) {
+3
View File
@@ -20,3 +20,6 @@ _cgo_export.*
_testmain.go
*.exe
.idea/
*.iml
+15 -4
View File
@@ -1,6 +1,17 @@
language: go
sudo: false
go:
- 1.1
- 1.2
- tip
matrix:
include:
- go: 1.4
- 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" {
cfg := d.TLSClientConfig
if cfg == nil {
cfg = &tls.Config{ServerName: hostNoPort}
} else if cfg.ServerName == "" {
shallowCopy := *cfg
cfg = &shallowCopy
cfg := cloneTLSConfig(d.TLSClientConfig)
if cfg.ServerName == "" {
cfg.ServerName = hostNoPort
}
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.
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"
"strconv"
"time"
"unicode/utf8"
)
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
maxControlFramePayloadSize = 125
finalBit = 1 << 7
maskBit = 1 << 7
writeWait = time.Second
writeWait = time.Second
defaultReadBufferSize = 4096
defaultWriteBufferSize = 4096
@@ -43,6 +52,8 @@ const (
CloseMessageTooBig = 1009
CloseMandatoryExtension = 1010
CloseInternalServerErr = 1011
CloseServiceRestart = 1012
CloseTryAgainLater = 1013
CloseTLSHandshake = 1015
)
@@ -184,6 +195,29 @@ func isData(frameType int) bool {
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 {
for i := range b {
b[i] ^= key[pos&3]
@@ -204,24 +238,26 @@ type Conn struct {
subprotocol string
// Write fields
mu chan bool // used as mutex to protect write to conn and closeSent
closeSent bool // true if close message was sent
// Message writer fields.
mu chan bool // used as mutex to protect write to conn and closeSent
closeSent bool // whether close message was sent
writeErr error
writeBuf []byte // frame is constructed in this buffer.
writePos int // end of data in writeBuf.
writeFrameType int // type of the current frame.
writeSeq int // incremented to invalidate message writers.
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
readErr error
br *bufio.Reader
readRemaining int64 // bytes remaining in current frame.
readFinal bool // true the current message has more frames.
readSeq int // incremented to invalidate message readers.
readLength int64 // Message size.
readLimit int64 // Maximum message size.
readMaskPos int
@@ -229,6 +265,10 @@ type Conn struct {
handlePong func(string) error
handlePing func(string) error
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 {
@@ -238,19 +278,23 @@ func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int)
if readBufferSize == 0 {
readBufferSize = defaultReadBufferSize
}
if readBufferSize < maxControlFramePayloadSize {
readBufferSize = maxControlFramePayloadSize
}
if writeBufferSize == 0 {
writeBufferSize = defaultWriteBufferSize
}
c := &Conn{
isServer: isServer,
br: bufio.NewReaderSize(conn, readBufferSize),
conn: conn,
mu: mu,
readFinal: true,
writeBuf: make([]byte, writeBufferSize+maxFrameHeaderSize),
writeFrameType: noFrame,
writePos: maxFrameHeaderSize,
isServer: isServer,
br: bufio.NewReaderSize(conn, readBufferSize),
conn: conn,
mu: mu,
readFinal: true,
writeBuf: make([]byte, writeBufferSize+maxFrameHeaderSize),
writeFrameType: noFrame,
writePos: maxFrameHeaderSize,
enableWriteCompression: true,
}
c.SetPingHandler(nil)
c.SetPongHandler(nil)
@@ -364,8 +408,8 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
return hideTempErr(err)
}
// NextWriter returns a writer for the next message to send. The writer's
// Close method flushes the complete message to the network.
// NextWriter returns a writer for the next message to send. The writer's Close
// method flushes the complete message to the network.
//
// There can be at most one open writer on a connection. NextWriter closes the
// 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
}
if c.writeFrameType != noFrame {
if err := c.flushFrame(true, nil); err != nil {
// Close previous writer if not already closed by the application. It's
// 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
}
}
@@ -385,16 +433,32 @@ func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
}
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 {
length := c.writePos - maxFrameHeaderSize + len(extra)
// Check for invalid control frames.
if isControl(c.writeFrameType) &&
(!final || length > maxControlFramePayloadSize) {
c.writeSeq++
c.messageWriter = nil
c.writer = nil
c.writeFrameType = noFrame
c.writePos = maxFrameHeaderSize
return errInvalidControlFrame
@@ -404,6 +468,11 @@ func (c *Conn) flushFrame(final bool, extra []byte) error {
if final {
b0 |= finalBit
}
if c.writeCompress {
b0 |= rsv1Bit
}
c.writeCompress = false
b1 := byte(0)
if !c.isServer {
b1 |= maskBit
@@ -462,20 +531,18 @@ func (c *Conn) flushFrame(final bool, extra []byte) error {
c.writePos = maxFrameHeaderSize
c.writeFrameType = continuationFrame
if final {
c.writeSeq++
c.messageWriter = nil
c.writer = nil
c.writeFrameType = noFrame
}
return c.writeErr
}
type messageWriter struct {
c *Conn
seq int
}
type messageWriter struct{ c *Conn }
func (w messageWriter) err() error {
func (w *messageWriter) err() error {
c := w.c
if c.writeSeq != w.seq {
if c.messageWriter != w {
return errWriteClosed
}
if c.writeErr != nil {
@@ -484,7 +551,7 @@ func (w messageWriter) err() error {
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
if n <= 0 {
if err := w.c.flushFrame(false, nil); err != nil {
@@ -498,14 +565,14 @@ func (w messageWriter) ncopy(max int) (int, error) {
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 {
return 0, err
}
if len(p) > 2*len(w.c.writeBuf) && w.c.isServer {
// Don't buffer large messages.
err := w.c.flushFrame(final, p)
err := w.c.flushFrame(false, p)
if err != nil {
return 0, err
}
@@ -525,11 +592,7 @@ func (w messageWriter) write(final bool, p []byte) (int, error) {
return nn, nil
}
func (w messageWriter) Write(p []byte) (int, error) {
return w.write(false, p)
}
func (w messageWriter) WriteString(p string) (int, error) {
func (w *messageWriter) WriteString(p string) (int, error) {
if err := w.err(); err != nil {
return 0, err
}
@@ -547,7 +610,7 @@ func (w messageWriter) WriteString(p string) (int, error) {
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 {
return 0, err
}
@@ -572,7 +635,7 @@ func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) {
return nn, err
}
func (w messageWriter) Close() error {
func (w *messageWriter) Close() error {
if err := w.err(); err != nil {
return err
}
@@ -582,20 +645,22 @@ func (w messageWriter) Close() error {
// WriteMessage is a helper method for getting a writer using NextWriter,
// writing the message and closing the writer.
func (c *Conn) WriteMessage(messageType int, data []byte) error {
wr, err := c.NextWriter(messageType)
w, err := c.NextWriter(messageType)
if err != nil {
return err
}
w := wr.(messageWriter)
if _, err := w.write(true, data); err != nil {
if _, ok := w.(*messageWriter); ok && c.isServer {
// 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
}
if c.writeSeq == w.seq {
if err := c.flushFrame(true, nil); err != nil {
return err
}
if _, err = w.Write(data); err != nil {
return err
}
return nil
return w.Close()
}
// SetWriteDeadline sets the write deadline on the underlying network
@@ -609,22 +674,6 @@ func (c *Conn) SetWriteDeadline(t time.Time) error {
// 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) {
// 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.
var b [8]byte
if err := c.readFull(b[:2]); err != nil {
p, err := c.read(2)
if err != nil {
return noFrame, err
}
final := b[0]&finalBit != 0
frameType := int(b[0] & 0xf)
reserved := int((b[0] >> 4) & 0x7)
mask := b[1]&maskBit != 0
c.readRemaining = int64(b[1] & 0x7f)
final := p[0]&finalBit != 0
frameType := int(p[0] & 0xf)
mask := p[1]&maskBit != 0
c.readRemaining = int64(p[1] & 0x7f)
if reserved != 0 {
return noFrame, c.handleProtocolError("unexpected reserved bits " + strconv.Itoa(reserved))
c.readDecompress = false
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 {
@@ -678,15 +732,17 @@ func (c *Conn) advanceFrame() (int, error) {
switch c.readRemaining {
case 126:
if err := c.readFull(b[:2]); err != nil {
p, err := c.read(2)
if err != nil {
return noFrame, err
}
c.readRemaining = int64(binary.BigEndian.Uint16(b[:2]))
c.readRemaining = int64(binary.BigEndian.Uint16(p))
case 127:
if err := c.readFull(b[:8]); err != nil {
p, err := c.read(8)
if err != nil {
return noFrame, err
}
c.readRemaining = int64(binary.BigEndian.Uint64(b[:8]))
c.readRemaining = int64(binary.BigEndian.Uint64(p))
}
// 4. Handle frame masking.
@@ -697,9 +753,11 @@ func (c *Conn) advanceFrame() (int, error) {
if mask {
c.readMaskPos = 0
if err := c.readFull(c.readMaskKey[:]); err != nil {
p, err := c.read(len(c.readMaskKey))
if err != nil {
return noFrame, err
}
copy(c.readMaskKey[:], p)
}
// 5. For text and binary messages, enforce read limit and return.
@@ -719,9 +777,9 @@ func (c *Conn) advanceFrame() (int, error) {
var payload []byte
if c.readRemaining > 0 {
payload = make([]byte, c.readRemaining)
payload, err = c.read(int(c.readRemaining))
c.readRemaining = 0
if err := c.readFull(payload); err != nil {
if err != nil {
return noFrame, err
}
if c.isServer {
@@ -747,7 +805,13 @@ func (c *Conn) advanceFrame() (int, error) {
if len(payload) >= 2 {
echoMessage = payload[:2]
closeCode = int(binary.BigEndian.Uint16(payload))
if !isValidReceivedCloseCode(closeCode) {
return noFrame, c.handleProtocolError("invalid close code")
}
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))
return noFrame, &CloseError{Code: closeCode, Text: closeText}
@@ -773,7 +837,7 @@ func (c *Conn) handleProtocolError(message string) error {
// this method return the same error.
func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
c.readSeq++
c.messageReader = nil
c.readLength = 0
for c.readErr == nil {
@@ -783,7 +847,12 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
break
}
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
}
type messageReader struct {
c *Conn
seq int
}
type messageReader struct{ c *Conn }
func (r messageReader) Read(b []byte) (int, error) {
if r.seq != r.c.readSeq {
func (r *messageReader) Read(b []byte) (int, error) {
c := r.c
if c.messageReader != r {
return 0, io.EOF
}
for r.c.readErr == nil {
for c.readErr == nil {
if r.c.readRemaining > 0 {
if int64(len(b)) > r.c.readRemaining {
b = b[:r.c.readRemaining]
if c.readRemaining > 0 {
if int64(len(b)) > c.readRemaining {
b = b[:c.readRemaining]
}
n, err := r.c.br.Read(b)
r.c.readErr = hideTempErr(err)
if r.c.isServer {
r.c.readMaskPos = maskBytes(r.c.readMaskKey, r.c.readMaskPos, b[:n])
n, err := c.br.Read(b)
c.readErr = hideTempErr(err)
if c.isServer {
c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n])
}
r.c.readRemaining -= int64(n)
return n, r.c.readErr
c.readRemaining -= int64(n)
if c.readRemaining > 0 && c.readErr == io.EOF {
c.readErr = errUnexpectedEOF
}
return n, c.readErr
}
if r.c.readFinal {
r.c.readSeq++
if c.readFinal {
c.messageReader = nil
return 0, io.EOF
}
frameType, err := r.c.advanceFrame()
frameType, err := c.advanceFrame()
switch {
case err != nil:
r.c.readErr = hideTempErr(err)
c.readErr = hideTempErr(err)
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
if err == io.EOF && r.seq == r.c.readSeq {
err := c.readErr
if err == io.EOF && c.messageReader == r {
err = errUnexpectedEOF
}
return 0, err
@@ -872,6 +941,11 @@ func (c *Conn) SetReadLimit(limit int64) {
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.
// The appData argument to h is the PING frame application data. The default
// ping handler sends a pong to the peer.
@@ -890,6 +964,11 @@ func (c *Conn) SetPingHandler(h func(appData string) error) {
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.
// The appData argument to h is the PONG frame application data. The default
// 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) {
const bufSize = 512
+12 -8
View File
@@ -85,18 +85,22 @@
// and pong. Call the connection WriteControl, WriteMessage or NextWriter
// 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
// peer and returning a *CloseError from the the NextReader, ReadMessage or the
// message Read method.
//
// The application must read the connection to process ping 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
// Connections handle received ping and pong messages by invoking callback
// functions set with SetPingHandler and SetPongHandler methods. The callback
// 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:
//
// func readLoop(c *websocket.Conn) {
+7 -1
View File
@@ -12,6 +12,11 @@ import (
"github.com/gorilla/websocket"
)
var (
c *websocket.Conn
req *http.Request
)
// The websocket.IsUnexpectedCloseError function is useful for identifying
// application and protocol errors.
//
@@ -20,7 +25,8 @@ import (
// only expected close message from the client has the code
// websocket.CloseGoingAway. All other other close messages are likely the
// 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 {
messageType, p, err := c.ReadMessage()
if err != nil {
+73 -2
View File
@@ -1,8 +1,8 @@
# Chat Example
This application shows how to use use the
[websocket](https://github.com/gorilla/websocket) package and
[jQuery](http://jquery.com) to implement a simple web chat application.
[websocket](https://github.com/gorilla/websocket) package to implement a simple
web chat application.
## Running the example
@@ -18,3 +18,74 @@ using the following commands.
$ go run *.go
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
import (
"github.com/gorilla/websocket"
"bytes"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
@@ -25,62 +27,86 @@ const (
maxMessageSize = 512
)
var (
newline = []byte{'\n'}
space = []byte{' '}
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// connection is an middleman between the websocket connection and the hub.
type connection struct {
// Client is an middleman between the websocket connection and the hub.
type Client struct {
hub *Hub
// The websocket connection.
ws *websocket.Conn
conn *websocket.Conn
// Buffered channel of outbound messages.
send chan []byte
}
// readPump pumps messages from the websocket connection to the hub.
func (c *connection) readPump() {
func (c *Client) readPump() {
defer func() {
h.unregister <- c
c.ws.Close()
c.hub.unregister <- c
c.conn.Close()
}()
c.ws.SetReadLimit(maxMessageSize)
c.ws.SetReadDeadline(time.Now().Add(pongWait))
c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := c.ws.ReadMessage()
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
log.Printf("error: %v", err)
}
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.
func (c *connection) write(mt int, payload []byte) error {
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
return c.ws.WriteMessage(mt, payload)
func (c *Client) write(mt int, payload []byte) error {
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
return c.conn.WriteMessage(mt, payload)
}
// writePump pumps messages from the hub to the websocket connection.
func (c *connection) writePump() {
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.ws.Close()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
if !ok {
// The hub closed the channel.
c.write(websocket.CloseMessage, []byte{})
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
}
case <-ticker.C:
@@ -92,14 +118,14 @@ func (c *connection) writePump() {
}
// serveWs handles websocket requests from the peer.
func serveWs(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
c := &connection{send: make(chan []byte, 256), ws: ws}
h.register <- c
go c.writePump()
c.readPump()
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
client.hub.register <- client
go client.writePump()
client.readPump()
}
+30 -24
View File
@@ -2,47 +2,53 @@
<html lang="en">
<head>
<title>Chat Example</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script type="text/javascript">
$(function() {
window.onload = function () {
var conn;
var msg = $("#msg");
var log = $("#log");
var msg = document.getElementById("msg");
var log = document.getElementById("log");
function appendLog(msg) {
var d = log[0]
var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
msg.appendTo(log)
function appendLog(item) {
var doScroll = log.scrollTop === log.scrollHeight - log.clientHeight;
log.appendChild(item);
if (doScroll) {
d.scrollTop = d.scrollHeight - d.clientHeight;
log.scrollTop = log.scrollHeight - log.clientHeight;
}
}
$("#form").submit(function() {
document.getElementById("form").onsubmit = function () {
if (!conn) {
return false;
}
if (!msg.val()) {
if (!msg.value) {
return false;
}
conn.send(msg.val());
msg.val("");
return false
});
conn.send(msg.value);
msg.value = "";
return false;
};
if (window["WebSocket"]) {
conn = new WebSocket("ws://{{$}}/ws");
conn.onclose = function(evt) {
appendLog($("<div><b>Connection closed.</b></div>"))
}
conn.onmessage = function(evt) {
appendLog($("<div/>").text(evt.data))
}
conn.onclose = function (evt) {
var item = document.createElement("div");
item.innerHTML = "<b>Connection closed.</b>";
appendLog(item);
};
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 {
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>
<style type="text/css">
html {
+29 -27
View File
@@ -4,46 +4,48 @@
package main
// hub maintains the set of active connections and broadcasts messages to the
// connections.
type hub struct {
// Registered connections.
connections map[*connection]bool
// hub maintains the set of active clients and broadcasts messages to the
// clients.
type Hub struct {
// Registered clients.
clients map[*Client]bool
// Inbound messages from the connections.
// Inbound messages from the clients.
broadcast chan []byte
// Register requests from the connections.
register chan *connection
// Register requests from the clients.
register chan *Client
// Unregister requests from connections.
unregister chan *connection
// Unregister requests from clients.
unregister chan *Client
}
var h = hub{
broadcast: make(chan []byte),
register: make(chan *connection),
unregister: make(chan *connection),
connections: make(map[*connection]bool),
func newHub() *Hub {
return &Hub{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
}
func (h *hub) run() {
func (h *Hub) run() {
for {
select {
case c := <-h.register:
h.connections[c] = true
case c := <-h.unregister:
if _, ok := h.connections[c]; ok {
delete(h.connections, c)
close(c.send)
case client := <-h.register:
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case m := <-h.broadcast:
for c := range h.connections {
case message := <-h.broadcast:
for client := range h.clients {
select {
case c.send <- m:
case client.send <- message:
default:
close(c.send)
delete(h.connections, c)
close(client.send)
delete(h.clients, client)
}
}
}
+8 -4
View File
@@ -12,9 +12,10 @@ import (
)
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) {
log.Println(r.URL)
if r.URL.Path != "/" {
http.Error(w, "Not found", 404)
return
@@ -24,14 +25,17 @@ func serveHome(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
homeTempl.Execute(w, r.Host)
homeTemplate.Execute(w, r.Host)
}
func main() {
flag.Parse()
go h.run()
hub := newHub()
go hub.run()
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)
if err != nil {
log.Fatal("ListenAndServe: ", err)
+1 -1
View File
@@ -120,7 +120,7 @@ func serveWs(w http.ResponseWriter, r *http.Request) {
}
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)
}
+9 -1
View File
@@ -53,6 +53,7 @@ func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status in
if u.Error != nil {
u.Error(w, r, status, err)
} else {
w.Header().Set("Sec-Websocket-Version", "13")
http.Error(w, http.StatusText(status), status)
}
return nil, err
@@ -99,7 +100,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
if r.Method != "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")
}
@@ -251,3 +252,10 @@ func Subprotocols(r *http.Request) []string {
}
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"
)
// 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")
func computeAcceptKey(challengeKey string) string {
@@ -42,3 +29,186 @@ func generateChallengeKey() (string, error) {
}
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 (
"net/http"
"reflect"
"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)
}
}
}