Update golang.org/x/net to fix godep reproducability problem

We are debugging why in #14677 but basically godep restore is ending up
on master and no the commit specified in godeps. (reproducable with just
go get, so don't blame godep right off)

To unbreak things this updates golang.org/x/net while we continue to dig
deeping into the core problem...
This commit is contained in:
Eric Paris 2015-09-29 11:16:55 -04:00
parent 7b428d8bcd
commit 17d54f42fe
27 changed files with 1413 additions and 796 deletions

6
Godeps/Godeps.json generated
View File

@ -578,15 +578,15 @@
}, },
{ {
"ImportPath": "golang.org/x/net/context", "ImportPath": "golang.org/x/net/context",
"Rev": "cbcac7bb8415db9b6cb4d1ebab1dc9afbd688b97" "Rev": "c2528b2dd8352441850638a8bb678c2ad056fd3e"
}, },
{ {
"ImportPath": "golang.org/x/net/html", "ImportPath": "golang.org/x/net/html",
"Rev": "cbcac7bb8415db9b6cb4d1ebab1dc9afbd688b97" "Rev": "c2528b2dd8352441850638a8bb678c2ad056fd3e"
}, },
{ {
"ImportPath": "golang.org/x/net/websocket", "ImportPath": "golang.org/x/net/websocket",
"Rev": "cbcac7bb8415db9b6cb4d1ebab1dc9afbd688b97" "Rev": "c2528b2dd8352441850638a8bb678c2ad056fd3e"
}, },
{ {
"ImportPath": "golang.org/x/oauth2", "ImportPath": "golang.org/x/oauth2",

View File

@ -64,18 +64,21 @@ type Context interface {
// //
// Done is provided for use in select statements: // Done is provided for use in select statements:
// //
// // DoSomething calls DoSomethingSlow and returns as soon as // // Stream generates values with DoSomething and sends them to out
// // it returns or ctx.Done is closed. // // until DoSomething returns an error or ctx.Done is closed.
// func DoSomething(ctx context.Context) (Result, error) { // func Stream(ctx context.Context, out <-chan Value) error {
// c := make(chan Result, 1) // for {
// go func() { c <- DoSomethingSlow(ctx) }() // v, err := DoSomething(ctx)
// select { // if err != nil {
// case res := <-c: // return err
// return res, nil // }
// case <-ctx.Done(): // select {
// return nil, ctx.Err() // case <-ctx.Done():
// } // return ctx.Err()
// } // case out <- v:
// }
// }
// }
// //
// See http://blog.golang.org/pipelines for more examples of how to use // See http://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancelation. // a Done channel for cancelation.
@ -202,6 +205,9 @@ type CancelFunc func()
// WithCancel returns a copy of parent with a new Done channel. The returned // WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called // context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first. // or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent) c := newCancelCtx(parent)
propagateCancel(parent, &c) propagateCancel(parent, &c)
@ -262,6 +268,19 @@ func parentCancelCtx(parent Context) (*cancelCtx, bool) {
} }
} }
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
// A canceler is a context type that can be canceled directly. The // A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx. // implementations are *cancelCtx and *timerCtx.
type canceler interface { type canceler interface {
@ -316,13 +335,7 @@ func (c *cancelCtx) cancel(removeFromParent bool, err error) {
c.mu.Unlock() c.mu.Unlock()
if removeFromParent { if removeFromParent {
if p, ok := parentCancelCtx(c.Context); ok { removeChild(c.Context, c)
p.mu.Lock()
if p.children != nil {
delete(p.children, c)
}
p.mu.Unlock()
}
} }
} }
@ -333,9 +346,8 @@ func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// cancel function is called, or when the parent context's Done channel is // cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first. // closed, whichever happens first.
// //
// Canceling this context releases resources associated with the deadline // Canceling this context releases resources associated with it, so code should
// timer, so code should call cancel as soon as the operations running in this // call cancel as soon as the operations running in this Context complete.
// Context complete.
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) { func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) { if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
// The current deadline is already sooner than the new one. // The current deadline is already sooner than the new one.
@ -380,7 +392,11 @@ func (c *timerCtx) String() string {
} }
func (c *timerCtx) cancel(removeFromParent bool, err error) { func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(removeFromParent, err) c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock() c.mu.Lock()
if c.timer != nil { if c.timer != nil {
c.timer.Stop() c.timer.Stop()
@ -391,9 +407,8 @@ func (c *timerCtx) cancel(removeFromParent bool, err error) {
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)). // WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
// //
// Canceling this context releases resources associated with the deadline // Canceling this context releases resources associated with it, so code should
// timer, so code should call cancel as soon as the operations running in this // call cancel as soon as the operations running in this Context complete:
// Context complete:
// //
// func slowOperationWithTimeout(ctx context.Context) (Result, error) { // func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) // ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)

View File

@ -375,7 +375,7 @@ func TestAllocs(t *testing.T) {
<-c.Done() <-c.Done()
}, },
limit: 8, limit: 8,
gccgoLimit: 13, gccgoLimit: 15,
}, },
{ {
desc: "WithCancel(bg)", desc: "WithCancel(bg)",
@ -551,3 +551,25 @@ func testLayers(t *testing.T, seed int64, testTimeout bool) {
checkValues("after cancel") checkValues("after cancel")
} }
} }
func TestCancelRemoves(t *testing.T) {
checkChildren := func(when string, ctx Context, want int) {
if got := len(ctx.(*cancelCtx).children); got != want {
t.Errorf("%s: context has %d children, want %d", when, got, want)
}
}
ctx, _ := WithCancel(Background())
checkChildren("after creation", ctx, 0)
_, cancel := WithCancel(ctx)
checkChildren("with WithCancel child ", ctx, 1)
cancel()
checkChildren("after cancelling WithCancel child", ctx, 0)
ctx, _ = WithCancel(Background())
checkChildren("after creation", ctx, 0)
_, cancel = WithTimeout(ctx, 60*time.Minute)
checkChildren("with WithTimeout child ", ctx, 1)
cancel()
checkChildren("after cancelling WithTimeout child", ctx, 0)
}

View File

@ -0,0 +1,18 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.5
package ctxhttp
import "net/http"
func canceler(client *http.Client, req *http.Request) func() {
ch := make(chan struct{})
req.Cancel = ch
return func() {
close(ch)
}
}

View File

@ -0,0 +1,23 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.5
package ctxhttp
import "net/http"
type requestCanceler interface {
CancelRequest(*http.Request)
}
func canceler(client *http.Client, req *http.Request) func() {
rc, ok := client.Transport.(requestCanceler)
if !ok {
return func() {}
}
return func() {
rc.CancelRequest(req)
}
}

View File

@ -0,0 +1,79 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package ctxhttp provides helper functions for performing context-aware HTTP requests.
package ctxhttp
import (
"io"
"net/http"
"net/url"
"strings"
"golang.org/x/net/context"
)
// Do sends an HTTP request with the provided http.Client and returns an HTTP response.
// If the client is nil, http.DefaultClient is used.
// If the context is canceled or times out, ctx.Err() will be returned.
func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
if client == nil {
client = http.DefaultClient
}
// Request cancelation changed in Go 1.5, see cancelreq.go and cancelreq_go14.go.
cancel := canceler(client, req)
type responseAndError struct {
resp *http.Response
err error
}
result := make(chan responseAndError, 1)
go func() {
resp, err := client.Do(req)
result <- responseAndError{resp, err}
}()
select {
case <-ctx.Done():
cancel()
return nil, ctx.Err()
case r := <-result:
return r.resp, r.err
}
}
// Get issues a GET request via the Do function.
func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return Do(ctx, client, req)
}
// Head issues a HEAD request via the Do function.
func Head(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
req, err := http.NewRequest("HEAD", url, nil)
if err != nil {
return nil, err
}
return Do(ctx, client, req)
}
// Post issues a POST request via the Do function.
func Post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", bodyType)
return Do(ctx, client, req)
}
// PostForm issues a POST request via the Do function.
func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) {
return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}

View File

@ -0,0 +1,72 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ctxhttp
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
"golang.org/x/net/context"
)
const (
requestDuration = 100 * time.Millisecond
requestBody = "ok"
)
func TestNoTimeout(t *testing.T) {
ctx := context.Background()
resp, err := doRequest(ctx)
if resp == nil || err != nil {
t.Fatalf("error received from client: %v %v", err, resp)
}
}
func TestCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(requestDuration / 2)
cancel()
}()
resp, err := doRequest(ctx)
if resp != nil || err == nil {
t.Fatalf("expected error, didn't get one. resp: %v", resp)
}
if err != ctx.Err() {
t.Fatalf("expected error from context but got: %v", err)
}
}
func TestCancelAfterRequest(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
resp, err := doRequest(ctx)
// Cancel before reading the body.
// Request.Body should still be readable after the context is canceled.
cancel()
b, err := ioutil.ReadAll(resp.Body)
if err != nil || string(b) != requestBody {
t.Fatalf("could not read body: %q %v", b, err)
}
}
func doRequest(ctx context.Context) (*http.Response, error) {
var okHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(requestDuration)
w.Write([]byte(requestBody))
})
serv := httptest.NewServer(okHandler)
defer serv.Close()
return Get(ctx, nil, serv.URL)
}

View File

@ -284,8 +284,8 @@ func (t *table) push(i uint32, depth int) bool {
} }
// The lists of element names and attribute keys were taken from // The lists of element names and attribute keys were taken from
// http://www.whatwg.org/specs/web-apps/current-work/multipage/section-index.html // https://html.spec.whatwg.org/multipage/indices.html#index
// as of the "HTML Living Standard - Last Updated 30 May 2012" version. // as of the "HTML Living Standard - Last Updated 21 February 2015" version.
var elements = []string{ var elements = []string{
"a", "a",
@ -352,6 +352,7 @@ var elements = []string{
"map", "map",
"mark", "mark",
"menu", "menu",
"menuitem",
"meta", "meta",
"meter", "meter",
"nav", "nav",
@ -385,6 +386,7 @@ var elements = []string{
"table", "table",
"tbody", "tbody",
"td", "td",
"template",
"textarea", "textarea",
"tfoot", "tfoot",
"th", "th",
@ -400,7 +402,10 @@ var elements = []string{
"wbr", "wbr",
} }
// https://html.spec.whatwg.org/multipage/indices.html#attributes-3
var attributes = []string{ var attributes = []string{
"abbr",
"accept", "accept",
"accept-charset", "accept-charset",
"accesskey", "accesskey",
@ -410,7 +415,6 @@ var attributes = []string{
"autocomplete", "autocomplete",
"autofocus", "autofocus",
"autoplay", "autoplay",
"border",
"challenge", "challenge",
"charset", "charset",
"checked", "checked",
@ -452,7 +456,7 @@ var attributes = []string{
"http-equiv", "http-equiv",
"icon", "icon",
"id", "id",
"inert", "inputmode",
"ismap", "ismap",
"itemid", "itemid",
"itemprop", "itemprop",
@ -473,6 +477,7 @@ var attributes = []string{
"mediagroup", "mediagroup",
"method", "method",
"min", "min",
"minlength",
"multiple", "multiple",
"muted", "muted",
"name", "name",
@ -500,6 +505,8 @@ var attributes = []string{
"shape", "shape",
"size", "size",
"sizes", "sizes",
"sortable",
"sorted",
"span", "span",
"src", "src",
"srcdoc", "srcdoc",
@ -521,6 +528,8 @@ var attributes = []string{
var eventHandlers = []string{ var eventHandlers = []string{
"onabort", "onabort",
"onautocomplete",
"onautocompleteerror",
"onafterprint", "onafterprint",
"onbeforeprint", "onbeforeprint",
"onbeforeunload", "onbeforeunload",
@ -552,6 +561,7 @@ var eventHandlers = []string{
"onkeydown", "onkeydown",
"onkeypress", "onkeypress",
"onkeyup", "onkeyup",
"onlanguagechange",
"onload", "onload",
"onloadeddata", "onloadeddata",
"onloadedmetadata", "onloadedmetadata",
@ -580,11 +590,13 @@ var eventHandlers = []string{
"onseeking", "onseeking",
"onselect", "onselect",
"onshow", "onshow",
"onsort",
"onstalled", "onstalled",
"onstorage", "onstorage",
"onsubmit", "onsubmit",
"onsuspend", "onsuspend",
"ontimeupdate", "ontimeupdate",
"ontoggle",
"onunload", "onunload",
"onvolumechange", "onvolumechange",
"onwaiting", "onwaiting",

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ package atom
var testAtomList = []string{ var testAtomList = []string{
"a", "a",
"abbr", "abbr",
"abbr",
"accept", "accept",
"accept-charset", "accept-charset",
"accesskey", "accesskey",
@ -33,7 +34,6 @@ var testAtomList = []string{
"blink", "blink",
"blockquote", "blockquote",
"body", "body",
"border",
"br", "br",
"button", "button",
"canvas", "canvas",
@ -125,8 +125,8 @@ var testAtomList = []string{
"iframe", "iframe",
"image", "image",
"img", "img",
"inert",
"input", "input",
"inputmode",
"ins", "ins",
"isindex", "isindex",
"ismap", "ismap",
@ -160,12 +160,14 @@ var testAtomList = []string{
"media", "media",
"mediagroup", "mediagroup",
"menu", "menu",
"menuitem",
"meta", "meta",
"meter", "meter",
"method", "method",
"mglyph", "mglyph",
"mi", "mi",
"min", "min",
"minlength",
"mn", "mn",
"mo", "mo",
"ms", "ms",
@ -183,6 +185,8 @@ var testAtomList = []string{
"ol", "ol",
"onabort", "onabort",
"onafterprint", "onafterprint",
"onautocomplete",
"onautocompleteerror",
"onbeforeprint", "onbeforeprint",
"onbeforeunload", "onbeforeunload",
"onblur", "onblur",
@ -213,6 +217,7 @@ var testAtomList = []string{
"onkeydown", "onkeydown",
"onkeypress", "onkeypress",
"onkeyup", "onkeyup",
"onlanguagechange",
"onload", "onload",
"onloadeddata", "onloadeddata",
"onloadedmetadata", "onloadedmetadata",
@ -241,11 +246,13 @@ var testAtomList = []string{
"onseeking", "onseeking",
"onselect", "onselect",
"onshow", "onshow",
"onsort",
"onstalled", "onstalled",
"onstorage", "onstorage",
"onsubmit", "onsubmit",
"onsuspend", "onsuspend",
"ontimeupdate", "ontimeupdate",
"ontoggle",
"onunload", "onunload",
"onvolumechange", "onvolumechange",
"onwaiting", "onwaiting",
@ -291,6 +298,8 @@ var testAtomList = []string{
"size", "size",
"sizes", "sizes",
"small", "small",
"sortable",
"sorted",
"source", "source",
"spacer", "spacer",
"span", "span",
@ -315,6 +324,7 @@ var testAtomList = []string{
"target", "target",
"tbody", "tbody",
"td", "td",
"template",
"textarea", "textarea",
"tfoot", "tfoot",
"th", "th",

View File

@ -5,11 +5,12 @@
// Package charset provides common text encodings for HTML documents. // Package charset provides common text encodings for HTML documents.
// //
// The mapping from encoding labels to encodings is defined at // The mapping from encoding labels to encodings is defined at
// http://encoding.spec.whatwg.org. // https://encoding.spec.whatwg.org/.
package charset package charset
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"mime" "mime"
"strings" "strings"
@ -110,6 +111,18 @@ func NewReader(r io.Reader, contentType string) (io.Reader, error) {
return r, nil return r, nil
} }
// NewReaderLabel returns a reader that converts from the specified charset to
// UTF-8. It uses Lookup to find the encoding that corresponds to label, and
// returns an error if Lookup returns nil. It is suitable for use as
// encoding/xml.Decoder's CharsetReader function.
func NewReaderLabel(label string, input io.Reader) (io.Reader, error) {
e, _ := Lookup(label)
if e == nil {
return nil, fmt.Errorf("unsupported charset: %q", label)
}
return transform.NewReader(input, e.NewDecoder()), nil
}
func prescan(content []byte) (e encoding.Encoding, name string) { func prescan(content []byte) (e encoding.Encoding, name string) {
z := html.NewTokenizer(bytes.NewReader(content)) z := html.NewTokenizer(bytes.NewReader(content))
for { for {

View File

@ -6,6 +6,7 @@ package charset
import ( import (
"bytes" "bytes"
"encoding/xml"
"io/ioutil" "io/ioutil"
"runtime" "runtime"
"strings" "strings"
@ -213,3 +214,23 @@ func TestFromMeta(t *testing.T) {
} }
} }
} }
func TestXML(t *testing.T) {
const s = "<?xml version=\"1.0\" encoding=\"windows-1252\"?><a><Word>r\xe9sum\xe9</Word></a>"
d := xml.NewDecoder(strings.NewReader(s))
d.CharsetReader = NewReaderLabel
var a struct {
Word string
}
err := d.Decode(&a)
if err != nil {
t.Fatalf("Decode: %v", err)
}
want := "résumé"
if a.Word != want {
t.Errorf("got %q, want %q", a.Word, want)
}
}

View File

@ -6,7 +6,7 @@
package main package main
// Download http://encoding.spec.whatwg.org/encodings.json and use it to // Download https://encoding.spec.whatwg.org/encodings.json and use it to
// generate table.go. // generate table.go.
import ( import (
@ -27,7 +27,7 @@ type group struct {
Heading string Heading string
} }
const specURL = "http://encoding.spec.whatwg.org/encodings.json" const specURL = "https://encoding.spec.whatwg.org/encodings.json"
func main() { func main() {
resp, err := http.Get(specURL) resp, err := http.Get(specURL)

View File

@ -1 +1,9 @@
These test cases come from http://www.w3.org/International/tests/html5/the-input-byte-stream/results-basics These test cases come from
http://www.w3.org/International/tests/repository/html5/the-input-byte-stream/results-basics
Distributed under both the W3C Test Suite License
(http://www.w3.org/Consortium/Legal/2008/04-testsuite-license)
and the W3C 3-clause BSD License
(http://www.w3.org/Consortium/Legal/2008/03-bsd-license).
To contribute to a W3C Test Suite, see the policies and contribution
forms (http://www.w3.org/2004/10/27-testcases).

View File

@ -6,7 +6,7 @@ package html
// Section 12.2.3.2 of the HTML5 specification says "The following elements // Section 12.2.3.2 of the HTML5 specification says "The following elements
// have varying levels of special parsing rules". // have varying levels of special parsing rules".
// http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html#the-stack-of-open-elements // https://html.spec.whatwg.org/multipage/syntax.html#the-stack-of-open-elements
var isSpecialElementMap = map[string]bool{ var isSpecialElementMap = map[string]bool{
"address": true, "address": true,
"applet": true, "applet": true,
@ -24,7 +24,6 @@ var isSpecialElementMap = map[string]bool{
"center": true, "center": true,
"col": true, "col": true,
"colgroup": true, "colgroup": true,
"command": true,
"dd": true, "dd": true,
"details": true, "details": true,
"dir": true, "dir": true,
@ -73,17 +72,20 @@ var isSpecialElementMap = map[string]bool{
"script": true, "script": true,
"section": true, "section": true,
"select": true, "select": true,
"source": true,
"style": true, "style": true,
"summary": true, "summary": true,
"table": true, "table": true,
"tbody": true, "tbody": true,
"td": true, "td": true,
"template": true,
"textarea": true, "textarea": true,
"tfoot": true, "tfoot": true,
"th": true, "th": true,
"thead": true, "thead": true,
"title": true, "title": true,
"tr": true, "tr": true,
"track": true,
"ul": true, "ul": true,
"wbr": true, "wbr": true,
"xmp": true, "xmp": true,

View File

@ -90,8 +90,8 @@ example, to process each anchor node in depth-first order:
f(doc) f(doc)
The relevant specifications include: The relevant specifications include:
http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html and https://html.spec.whatwg.org/multipage/syntax.html and
http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html https://html.spec.whatwg.org/multipage/syntax.html#tokenization
*/ */
package html package html

View File

@ -8,7 +8,7 @@ package html
const longestEntityWithoutSemicolon = 6 const longestEntityWithoutSemicolon = 6
// entity is a map from HTML entity names to their values. The semicolon matters: // entity is a map from HTML entity names to their values. The semicolon matters:
// http://www.whatwg.org/specs/web-apps/current-work/multipage/named-character-references.html // https://html.spec.whatwg.org/multipage/syntax.html#named-character-references
// lists both "amp" and "amp;" as two separate entries. // lists both "amp" and "amp;" as two separate entries.
// //
// Note that the HTML5 list is larger than the HTML4 list at // Note that the HTML5 list is larger than the HTML4 list at

View File

@ -12,7 +12,7 @@ import (
// These replacements permit compatibility with old numeric entities that // These replacements permit compatibility with old numeric entities that
// assumed Windows-1252 encoding. // assumed Windows-1252 encoding.
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#consume-a-character-reference // https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference
var replacementTable = [...]rune{ var replacementTable = [...]rune{
'\u20AC', // First entry is what 0x80 should be replaced with. '\u20AC', // First entry is what 0x80 should be replaced with.
'\u0081', '\u0081',
@ -55,7 +55,7 @@ var replacementTable = [...]rune{
// Precondition: b[src] == '&' && dst <= src. // Precondition: b[src] == '&' && dst <= src.
// attribute should be true if parsing an attribute value. // attribute should be true if parsing an attribute value.
func unescapeEntity(b []byte, dst, src int, attribute bool) (dst1, src1 int) { func unescapeEntity(b []byte, dst, src int, attribute bool) (dst1, src1 int) {
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#consume-a-character-reference // https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference
// i starts at 1 because we already know that s[0] == '&'. // i starts at 1 because we already know that s[0] == '&'.
i, s := 1, b[src:] i, s := 1, b[src:]

View File

@ -14,7 +14,7 @@ import (
) )
// A parser implements the HTML5 parsing algorithm: // A parser implements the HTML5 parsing algorithm:
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#tree-construction // https://html.spec.whatwg.org/multipage/syntax.html#tree-construction
type parser struct { type parser struct {
// tokenizer provides the tokens for the parser. // tokenizer provides the tokens for the parser.
tokenizer *Tokenizer tokenizer *Tokenizer
@ -59,7 +59,7 @@ func (p *parser) top() *Node {
// Stop tags for use in popUntil. These come from section 12.2.3.2. // Stop tags for use in popUntil. These come from section 12.2.3.2.
var ( var (
defaultScopeStopTags = map[string][]a.Atom{ defaultScopeStopTags = map[string][]a.Atom{
"": {a.Applet, a.Caption, a.Html, a.Table, a.Td, a.Th, a.Marquee, a.Object}, "": {a.Applet, a.Caption, a.Html, a.Table, a.Td, a.Th, a.Marquee, a.Object, a.Template},
"math": {a.AnnotationXml, a.Mi, a.Mn, a.Mo, a.Ms, a.Mtext}, "math": {a.AnnotationXml, a.Mi, a.Mn, a.Mo, a.Ms, a.Mtext},
"svg": {a.Desc, a.ForeignObject, a.Title}, "svg": {a.Desc, a.ForeignObject, a.Title},
} }
@ -1037,15 +1037,15 @@ func inBodyIM(p *parser) bool {
func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) { func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) {
// This is the "adoption agency" algorithm, described at // This is the "adoption agency" algorithm, described at
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#adoptionAgency // https://html.spec.whatwg.org/multipage/syntax.html#adoptionAgency
// TODO: this is a fairly literal line-by-line translation of that algorithm. // TODO: this is a fairly literal line-by-line translation of that algorithm.
// Once the code successfully parses the comprehensive test suite, we should // Once the code successfully parses the comprehensive test suite, we should
// refactor this code to be more idiomatic. // refactor this code to be more idiomatic.
// Steps 1-3. The outer loop. // Steps 1-4. The outer loop.
for i := 0; i < 8; i++ { for i := 0; i < 8; i++ {
// Step 4. Find the formatting element. // Step 5. Find the formatting element.
var formattingElement *Node var formattingElement *Node
for j := len(p.afe) - 1; j >= 0; j-- { for j := len(p.afe) - 1; j >= 0; j-- {
if p.afe[j].Type == scopeMarkerNode { if p.afe[j].Type == scopeMarkerNode {
@ -1070,7 +1070,7 @@ func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) {
return return
} }
// Steps 5-6. Find the furthest block. // Steps 9-10. Find the furthest block.
var furthestBlock *Node var furthestBlock *Node
for _, e := range p.oe[feIndex:] { for _, e := range p.oe[feIndex:] {
if isSpecialElement(e) { if isSpecialElement(e) {
@ -1087,47 +1087,47 @@ func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) {
return return
} }
// Steps 7-8. Find the common ancestor and bookmark node. // Steps 11-12. Find the common ancestor and bookmark node.
commonAncestor := p.oe[feIndex-1] commonAncestor := p.oe[feIndex-1]
bookmark := p.afe.index(formattingElement) bookmark := p.afe.index(formattingElement)
// Step 9. The inner loop. Find the lastNode to reparent. // Step 13. The inner loop. Find the lastNode to reparent.
lastNode := furthestBlock lastNode := furthestBlock
node := furthestBlock node := furthestBlock
x := p.oe.index(node) x := p.oe.index(node)
// Steps 9.1-9.3. // Steps 13.1-13.2
for j := 0; j < 3; j++ { for j := 0; j < 3; j++ {
// Step 9.4. // Step 13.3.
x-- x--
node = p.oe[x] node = p.oe[x]
// Step 9.5. // Step 13.4 - 13.5.
if p.afe.index(node) == -1 { if p.afe.index(node) == -1 {
p.oe.remove(node) p.oe.remove(node)
continue continue
} }
// Step 9.6. // Step 13.6.
if node == formattingElement { if node == formattingElement {
break break
} }
// Step 9.7. // Step 13.7.
clone := node.clone() clone := node.clone()
p.afe[p.afe.index(node)] = clone p.afe[p.afe.index(node)] = clone
p.oe[p.oe.index(node)] = clone p.oe[p.oe.index(node)] = clone
node = clone node = clone
// Step 9.8. // Step 13.8.
if lastNode == furthestBlock { if lastNode == furthestBlock {
bookmark = p.afe.index(node) + 1 bookmark = p.afe.index(node) + 1
} }
// Step 9.9. // Step 13.9.
if lastNode.Parent != nil { if lastNode.Parent != nil {
lastNode.Parent.RemoveChild(lastNode) lastNode.Parent.RemoveChild(lastNode)
} }
node.AppendChild(lastNode) node.AppendChild(lastNode)
// Step 9.10. // Step 13.10.
lastNode = node lastNode = node
} }
// Step 10. Reparent lastNode to the common ancestor, // Step 14. Reparent lastNode to the common ancestor,
// or for misnested table nodes, to the foster parent. // or for misnested table nodes, to the foster parent.
if lastNode.Parent != nil { if lastNode.Parent != nil {
lastNode.Parent.RemoveChild(lastNode) lastNode.Parent.RemoveChild(lastNode)
@ -1139,13 +1139,13 @@ func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) {
commonAncestor.AppendChild(lastNode) commonAncestor.AppendChild(lastNode)
} }
// Steps 11-13. Reparent nodes from the furthest block's children // Steps 15-17. Reparent nodes from the furthest block's children
// to a clone of the formatting element. // to a clone of the formatting element.
clone := formattingElement.clone() clone := formattingElement.clone()
reparentChildren(clone, furthestBlock) reparentChildren(clone, furthestBlock)
furthestBlock.AppendChild(clone) furthestBlock.AppendChild(clone)
// Step 14. Fix up the list of active formatting elements. // Step 18. Fix up the list of active formatting elements.
if oldLoc := p.afe.index(formattingElement); oldLoc != -1 && oldLoc < bookmark { if oldLoc := p.afe.index(formattingElement); oldLoc != -1 && oldLoc < bookmark {
// Move the bookmark with the rest of the list. // Move the bookmark with the rest of the list.
bookmark-- bookmark--
@ -1153,13 +1153,15 @@ func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) {
p.afe.remove(formattingElement) p.afe.remove(formattingElement)
p.afe.insert(bookmark, clone) p.afe.insert(bookmark, clone)
// Step 15. Fix up the stack of open elements. // Step 19. Fix up the stack of open elements.
p.oe.remove(formattingElement) p.oe.remove(formattingElement)
p.oe.insert(p.oe.index(furthestBlock)+1, clone) p.oe.insert(p.oe.index(furthestBlock)+1, clone)
} }
} }
// inBodyEndTagOther performs the "any other end tag" algorithm for inBodyIM. // inBodyEndTagOther performs the "any other end tag" algorithm for inBodyIM.
// "Any other end tag" handling from 12.2.5.5 The rules for parsing tokens in foreign content
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inforeign
func (p *parser) inBodyEndTagOther(tagAtom a.Atom) { func (p *parser) inBodyEndTagOther(tagAtom a.Atom) {
for i := len(p.oe) - 1; i >= 0; i-- { for i := len(p.oe) - 1; i >= 0; i-- {
if p.oe[i].DataAtom == tagAtom { if p.oe[i].DataAtom == tagAtom {

View File

@ -14,7 +14,7 @@ import (
type writer interface { type writer interface {
io.Writer io.Writer
WriteByte(c byte) error // in Go 1.1, use io.ByteWriter io.ByteWriter
WriteString(string) (int, error) WriteString(string) (int, error)
} }

View File

@ -392,7 +392,7 @@ var tokenTests = []tokenTest{
"½", "½",
}, },
// Attribute tests: // Attribute tests:
// http://dev.w3.org/html5/spec/Overview.html#attributes-0 // http://dev.w3.org/html5/pf-summary/Overview.html#attributes
{ {
"Empty attribute", "Empty attribute",
`<input disabled FOO>`, `<input disabled FOO>`,

View File

@ -64,6 +64,20 @@ func Dial(url_, protocol, origin string) (ws *Conn, err error) {
return DialConfig(config) return DialConfig(config)
} }
var portMap = map[string]string{
"ws": "80",
"wss": "443",
}
func parseAuthority(location *url.URL) string {
if _, ok := portMap[location.Scheme]; ok {
if _, _, err := net.SplitHostPort(location.Host); err != nil {
return net.JoinHostPort(location.Host, portMap[location.Scheme])
}
}
return location.Host
}
// DialConfig opens a new client connection to a WebSocket with a config. // DialConfig opens a new client connection to a WebSocket with a config.
func DialConfig(config *Config) (ws *Conn, err error) { func DialConfig(config *Config) (ws *Conn, err error) {
var client net.Conn var client net.Conn
@ -75,10 +89,10 @@ func DialConfig(config *Config) (ws *Conn, err error) {
} }
switch config.Location.Scheme { switch config.Location.Scheme {
case "ws": case "ws":
client, err = net.Dial("tcp", config.Location.Host) client, err = net.Dial("tcp", parseAuthority(config.Location))
case "wss": case "wss":
client, err = tls.Dial("tcp", config.Location.Host, config.TlsConfig) client, err = tls.Dial("tcp", parseAuthority(config.Location), config.TlsConfig)
default: default:
err = ErrBadScheme err = ErrBadScheme
@ -89,6 +103,7 @@ func DialConfig(config *Config) (ws *Conn, err error) {
ws, err = NewClient(config, client) ws, err = NewClient(config, client)
if err != nil { if err != nil {
client.Close()
goto Error goto Error
} }
return return

View File

@ -157,6 +157,9 @@ func (buf hybiFrameReaderFactory) NewFrameReader() (frame frameReader, err error
if err != nil { if err != nil {
return return
} }
if lengthFields == 8 && i == 0 { // MSB must be zero when 7+64 bits
b &= 0x7f
}
header = append(header, b) header = append(header, b)
hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b) hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b)
} }
@ -264,7 +267,7 @@ type hybiFrameHandler struct {
payloadType byte payloadType byte
} }
func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (r frameReader, err error) { func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (frameReader, error) {
if handler.conn.IsServerConn() { if handler.conn.IsServerConn() {
// The client MUST mask all frames sent to the server. // The client MUST mask all frames sent to the server.
if frame.(*hybiFrameReader).header.MaskingKey == nil { if frame.(*hybiFrameReader).header.MaskingKey == nil {
@ -288,20 +291,19 @@ func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (r frameReader,
handler.payloadType = frame.PayloadType() handler.payloadType = frame.PayloadType()
case CloseFrame: case CloseFrame:
return nil, io.EOF return nil, io.EOF
case PingFrame: case PingFrame, PongFrame:
pingMsg := make([]byte, maxControlFramePayloadLength) b := make([]byte, maxControlFramePayloadLength)
n, err := io.ReadFull(frame, pingMsg) n, err := io.ReadFull(frame, b)
if err != nil && err != io.ErrUnexpectedEOF { if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return nil, err return nil, err
} }
io.Copy(ioutil.Discard, frame) io.Copy(ioutil.Discard, frame)
n, err = handler.WritePong(pingMsg[:n]) if frame.PayloadType() == PingFrame {
if err != nil { if _, err := handler.WritePong(b[:n]); err != nil {
return nil, err return nil, err
}
} }
return nil, nil return nil, nil
case PongFrame:
return nil, ErrNotImplemented
} }
return frame, nil return frame, nil
} }
@ -370,6 +372,23 @@ func generateNonce() (nonce []byte) {
return return
} }
// removeZone removes IPv6 zone identifer from host.
// E.g., "[fe80::1%en0]:8080" to "[fe80::1]:8080"
func removeZone(host string) string {
if !strings.HasPrefix(host, "[") {
return host
}
i := strings.LastIndex(host, "]")
if i < 0 {
return host
}
j := strings.LastIndex(host[:i], "%")
if j < 0 {
return host
}
return host[:j] + host[i:]
}
// getNonceAccept computes the base64-encoded SHA-1 of the concatenation of // getNonceAccept computes the base64-encoded SHA-1 of the concatenation of
// the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string. // the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string.
func getNonceAccept(nonce []byte) (expected []byte, err error) { func getNonceAccept(nonce []byte) (expected []byte, err error) {
@ -389,7 +408,10 @@ func getNonceAccept(nonce []byte) (expected []byte, err error) {
func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) { func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) {
bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n") bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n")
bw.WriteString("Host: " + config.Location.Host + "\r\n") // According to RFC 6874, an HTTP client, proxy, or other
// intermediary must remove any IPv6 zone identifier attached
// to an outgoing URI.
bw.WriteString("Host: " + removeZone(config.Location.Host) + "\r\n")
bw.WriteString("Upgrade: websocket\r\n") bw.WriteString("Upgrade: websocket\r\n")
bw.WriteString("Connection: Upgrade\r\n") bw.WriteString("Connection: Upgrade\r\n")
nonce := generateNonce() nonce := generateNonce()
@ -515,15 +537,15 @@ func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Reques
return http.StatusSwitchingProtocols, nil return http.StatusSwitchingProtocols, nil
} }
// Origin parses Origin header in "req". // Origin parses the Origin header in req.
// If origin is "null", returns (nil, nil). // If the Origin header is not set, it returns nil and nil.
func Origin(config *Config, req *http.Request) (*url.URL, error) { func Origin(config *Config, req *http.Request) (*url.URL, error) {
var origin string var origin string
switch config.Version { switch config.Version {
case ProtocolVersionHybi13: case ProtocolVersionHybi13:
origin = req.Header.Get("Origin") origin = req.Header.Get("Origin")
} }
if origin == "null" { if origin == "" {
return nil, nil return nil, nil
} }
return url.ParseRequestURI(origin) return url.ParseRequestURI(origin)

View File

@ -31,63 +31,74 @@ func TestSecWebSocketAccept(t *testing.T) {
} }
func TestHybiClientHandshake(t *testing.T) { func TestHybiClientHandshake(t *testing.T) {
b := bytes.NewBuffer([]byte{}) type test struct {
bw := bufio.NewWriter(b) url, host string
br := bufio.NewReader(strings.NewReader(`HTTP/1.1 101 Switching Protocols }
tests := []test{
{"ws://server.example.com/chat", "server.example.com"},
{"ws://127.0.0.1/chat", "127.0.0.1"},
}
if _, err := url.ParseRequestURI("http://[fe80::1%25lo0]"); err == nil {
tests = append(tests, test{"ws://[fe80::1%25lo0]/chat", "[fe80::1]"})
}
for _, tt := range tests {
var b bytes.Buffer
bw := bufio.NewWriter(&b)
br := bufio.NewReader(strings.NewReader(`HTTP/1.1 101 Switching Protocols
Upgrade: websocket Upgrade: websocket
Connection: Upgrade Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat Sec-WebSocket-Protocol: chat
`)) `))
var err error var err error
config := new(Config) var config Config
config.Location, err = url.ParseRequestURI("ws://server.example.com/chat") config.Location, err = url.ParseRequestURI(tt.url)
if err != nil { if err != nil {
t.Fatal("location url", err) t.Fatal("location url", err)
} }
config.Origin, err = url.ParseRequestURI("http://example.com") config.Origin, err = url.ParseRequestURI("http://example.com")
if err != nil { if err != nil {
t.Fatal("origin url", err) t.Fatal("origin url", err)
} }
config.Protocol = append(config.Protocol, "chat") config.Protocol = append(config.Protocol, "chat")
config.Protocol = append(config.Protocol, "superchat") config.Protocol = append(config.Protocol, "superchat")
config.Version = ProtocolVersionHybi13 config.Version = ProtocolVersionHybi13
config.handshakeData = map[string]string{
config.handshakeData = map[string]string{ "key": "dGhlIHNhbXBsZSBub25jZQ==",
"key": "dGhlIHNhbXBsZSBub25jZQ==", }
} if err := hybiClientHandshake(&config, br, bw); err != nil {
err = hybiClientHandshake(config, br, bw) t.Fatal("handshake", err)
if err != nil { }
t.Errorf("handshake failed: %v", err) req, err := http.ReadRequest(bufio.NewReader(&b))
} if err != nil {
req, err := http.ReadRequest(bufio.NewReader(b)) t.Fatal("read request", err)
if err != nil { }
t.Fatalf("read request: %v", err) if req.Method != "GET" {
} t.Errorf("request method expected GET, but got %s", req.Method)
if req.Method != "GET" { }
t.Errorf("request method expected GET, but got %q", req.Method) if req.URL.Path != "/chat" {
} t.Errorf("request path expected /chat, but got %s", req.URL.Path)
if req.URL.Path != "/chat" { }
t.Errorf("request path expected /chat, but got %q", req.URL.Path) if req.Proto != "HTTP/1.1" {
} t.Errorf("request proto expected HTTP/1.1, but got %s", req.Proto)
if req.Proto != "HTTP/1.1" { }
t.Errorf("request proto expected HTTP/1.1, but got %q", req.Proto) if req.Host != tt.host {
} t.Errorf("request host expected %s, but got %s", tt.host, req.Host)
if req.Host != "server.example.com" { }
t.Errorf("request Host expected server.example.com, but got %v", req.Host) var expectedHeader = map[string]string{
} "Connection": "Upgrade",
var expectedHeader = map[string]string{ "Upgrade": "websocket",
"Connection": "Upgrade", "Sec-Websocket-Key": config.handshakeData["key"],
"Upgrade": "websocket", "Origin": config.Origin.String(),
"Sec-Websocket-Key": config.handshakeData["key"], "Sec-Websocket-Protocol": "chat, superchat",
"Origin": config.Origin.String(), "Sec-Websocket-Version": fmt.Sprintf("%d", ProtocolVersionHybi13),
"Sec-Websocket-Protocol": "chat, superchat", }
"Sec-Websocket-Version": fmt.Sprintf("%d", ProtocolVersionHybi13), for k, v := range expectedHeader {
} if req.Header.Get(k) != v {
for k, v := range expectedHeader { t.Errorf("%s expected %s, but got %v", k, v, req.Header.Get(k))
if req.Header.Get(k) != v { }
t.Errorf(fmt.Sprintf("%s expected %q but got %q", k, v, req.Header.Get(k)))
} }
} }
} }
@ -326,7 +337,7 @@ func testHybiFrame(t *testing.T, testHeader, testPayload, testMaskedPayload []by
} }
payload := make([]byte, len(testPayload)) payload := make([]byte, len(testPayload))
_, err = r.Read(payload) _, err = r.Read(payload)
if err != nil { if err != nil && err != io.EOF {
t.Errorf("read %v", err) t.Errorf("read %v", err)
} }
if !bytes.Equal(testPayload, payload) { if !bytes.Equal(testPayload, payload) {
@ -363,13 +374,20 @@ func TestHybiShortBinaryFrame(t *testing.T) {
} }
func TestHybiControlFrame(t *testing.T) { func TestHybiControlFrame(t *testing.T) {
frameHeader := &hybiFrameHeader{Fin: true, OpCode: PingFrame}
payload := []byte("hello") payload := []byte("hello")
frameHeader := &hybiFrameHeader{Fin: true, OpCode: PingFrame}
testHybiFrame(t, []byte{0x89, 0x05}, payload, payload, frameHeader) testHybiFrame(t, []byte{0x89, 0x05}, payload, payload, frameHeader)
frameHeader = &hybiFrameHeader{Fin: true, OpCode: PingFrame}
testHybiFrame(t, []byte{0x89, 0x00}, nil, nil, frameHeader)
frameHeader = &hybiFrameHeader{Fin: true, OpCode: PongFrame} frameHeader = &hybiFrameHeader{Fin: true, OpCode: PongFrame}
testHybiFrame(t, []byte{0x8A, 0x05}, payload, payload, frameHeader) testHybiFrame(t, []byte{0x8A, 0x05}, payload, payload, frameHeader)
frameHeader = &hybiFrameHeader{Fin: true, OpCode: PongFrame}
testHybiFrame(t, []byte{0x8A, 0x00}, nil, nil, frameHeader)
frameHeader = &hybiFrameHeader{Fin: true, OpCode: CloseFrame} frameHeader = &hybiFrameHeader{Fin: true, OpCode: CloseFrame}
payload = []byte{0x03, 0xe8} // 1000 payload = []byte{0x03, 0xe8} // 1000
testHybiFrame(t, []byte{0x88, 0x02}, payload, payload, frameHeader) testHybiFrame(t, []byte{0x88, 0x02}, payload, payload, frameHeader)

View File

@ -74,7 +74,6 @@ func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) {
rwc, buf, err := w.(http.Hijacker).Hijack() rwc, buf, err := w.(http.Hijacker).Hijack()
if err != nil { if err != nil {
panic("Hijack failed: " + err.Error()) panic("Hijack failed: " + err.Error())
return
} }
// The server should abort the WebSocket connection if it finds // The server should abort the WebSocket connection if it finds
// the client did not send a handshake that matches with protocol // the client did not send a handshake that matches with protocol
@ -95,8 +94,8 @@ func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) {
// You might want to verify websocket.Conn.Config().Origin in the func. // You might want to verify websocket.Conn.Config().Origin in the func.
// If you use Server instead of Handler, you could call websocket.Origin and // If you use Server instead of Handler, you could call websocket.Origin and
// check the origin in your Handshake func. So, if you want to accept // check the origin in your Handshake func. So, if you want to accept
// non-browser client, which doesn't send Origin header, you could use Server // non-browser clients, which do not send an Origin header, set a
//. that doesn't check origin in its Handshake. // Server.Handshake that does not check the origin.
type Handler func(*Conn) type Handler func(*Conn)
func checkOrigin(config *Config, req *http.Request) (err error) { func checkOrigin(config *Config, req *http.Request) (err error) {

View File

@ -216,10 +216,11 @@ func (ws *Conn) Write(msg []byte) (n int, err error) {
// Close implements the io.Closer interface. // Close implements the io.Closer interface.
func (ws *Conn) Close() error { func (ws *Conn) Close() error {
err := ws.frameHandler.WriteClose(ws.defaultCloseStatus) err := ws.frameHandler.WriteClose(ws.defaultCloseStatus)
err1 := ws.rwc.Close()
if err != nil { if err != nil {
return err return err
} }
return ws.rwc.Close() return err1
} }
func (ws *Conn) IsClientConn() bool { return ws.request == nil } func (ws *Conn) IsClientConn() bool { return ws.request == nil }

View File

@ -13,15 +13,21 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"reflect"
"runtime"
"strings" "strings"
"sync" "sync"
"testing" "testing"
"time"
) )
var serverAddr string var serverAddr string
var once sync.Once var once sync.Once
func echoServer(ws *Conn) { io.Copy(ws, ws) } func echoServer(ws *Conn) {
defer ws.Close()
io.Copy(ws, ws)
}
type Count struct { type Count struct {
S string S string
@ -29,6 +35,7 @@ type Count struct {
} }
func countServer(ws *Conn) { func countServer(ws *Conn) {
defer ws.Close()
for { for {
var count Count var count Count
err := JSON.Receive(ws, &count) err := JSON.Receive(ws, &count)
@ -44,6 +51,55 @@ func countServer(ws *Conn) {
} }
} }
type testCtrlAndDataHandler struct {
hybiFrameHandler
}
func (h *testCtrlAndDataHandler) WritePing(b []byte) (int, error) {
h.hybiFrameHandler.conn.wio.Lock()
defer h.hybiFrameHandler.conn.wio.Unlock()
w, err := h.hybiFrameHandler.conn.frameWriterFactory.NewFrameWriter(PingFrame)
if err != nil {
return 0, err
}
n, err := w.Write(b)
w.Close()
return n, err
}
func ctrlAndDataServer(ws *Conn) {
defer ws.Close()
h := &testCtrlAndDataHandler{hybiFrameHandler: hybiFrameHandler{conn: ws}}
ws.frameHandler = h
go func() {
for i := 0; ; i++ {
var b []byte
if i%2 != 0 { // with or without payload
b = []byte(fmt.Sprintf("#%d-CONTROL-FRAME-FROM-SERVER", i))
}
if _, err := h.WritePing(b); err != nil {
break
}
if _, err := h.WritePong(b); err != nil { // unsolicited pong
break
}
time.Sleep(10 * time.Millisecond)
}
}()
b := make([]byte, 128)
for {
n, err := ws.Read(b)
if err != nil {
break
}
if _, err := ws.Write(b[:n]); err != nil {
break
}
}
}
func subProtocolHandshake(config *Config, req *http.Request) error { func subProtocolHandshake(config *Config, req *http.Request) error {
for _, proto := range config.Protocol { for _, proto := range config.Protocol {
if proto == "chat" { if proto == "chat" {
@ -63,6 +119,7 @@ func subProtoServer(ws *Conn) {
func startServer() { func startServer() {
http.Handle("/echo", Handler(echoServer)) http.Handle("/echo", Handler(echoServer))
http.Handle("/count", Handler(countServer)) http.Handle("/count", Handler(countServer))
http.Handle("/ctrldata", Handler(ctrlAndDataServer))
subproto := Server{ subproto := Server{
Handshake: subProtocolHandshake, Handshake: subProtocolHandshake,
Handler: Handler(subProtoServer), Handler: Handler(subProtoServer),
@ -339,3 +396,192 @@ func TestSmallBuffer(t *testing.T) {
} }
conn.Close() conn.Close()
} }
var parseAuthorityTests = []struct {
in *url.URL
out string
}{
{
&url.URL{
Scheme: "ws",
Host: "www.google.com",
},
"www.google.com:80",
},
{
&url.URL{
Scheme: "wss",
Host: "www.google.com",
},
"www.google.com:443",
},
{
&url.URL{
Scheme: "ws",
Host: "www.google.com:80",
},
"www.google.com:80",
},
{
&url.URL{
Scheme: "wss",
Host: "www.google.com:443",
},
"www.google.com:443",
},
// some invalid ones for parseAuthority. parseAuthority doesn't
// concern itself with the scheme unless it actually knows about it
{
&url.URL{
Scheme: "http",
Host: "www.google.com",
},
"www.google.com",
},
{
&url.URL{
Scheme: "http",
Host: "www.google.com:80",
},
"www.google.com:80",
},
{
&url.URL{
Scheme: "asdf",
Host: "127.0.0.1",
},
"127.0.0.1",
},
{
&url.URL{
Scheme: "asdf",
Host: "www.google.com",
},
"www.google.com",
},
}
func TestParseAuthority(t *testing.T) {
for _, tt := range parseAuthorityTests {
out := parseAuthority(tt.in)
if out != tt.out {
t.Errorf("got %v; want %v", out, tt.out)
}
}
}
type closerConn struct {
net.Conn
closed int // count of the number of times Close was called
}
func (c *closerConn) Close() error {
c.closed++
return c.Conn.Close()
}
func TestClose(t *testing.T) {
if runtime.GOOS == "plan9" {
t.Skip("see golang.org/issue/11454")
}
once.Do(startServer)
conn, err := net.Dial("tcp", serverAddr)
if err != nil {
t.Fatal("dialing", err)
}
cc := closerConn{Conn: conn}
client, err := NewClient(newConfig(t, "/echo"), &cc)
if err != nil {
t.Fatalf("WebSocket handshake: %v", err)
}
// set the deadline to ten minutes ago, which will have expired by the time
// client.Close sends the close status frame.
conn.SetDeadline(time.Now().Add(-10 * time.Minute))
if err := client.Close(); err == nil {
t.Errorf("ws.Close(): expected error, got %v", err)
}
if cc.closed < 1 {
t.Fatalf("ws.Close(): expected underlying ws.rwc.Close to be called > 0 times, got: %v", cc.closed)
}
}
var originTests = []struct {
req *http.Request
origin *url.URL
}{
{
req: &http.Request{
Header: http.Header{
"Origin": []string{"http://www.example.com"},
},
},
origin: &url.URL{
Scheme: "http",
Host: "www.example.com",
},
},
{
req: &http.Request{},
},
}
func TestOrigin(t *testing.T) {
conf := newConfig(t, "/echo")
conf.Version = ProtocolVersionHybi13
for i, tt := range originTests {
origin, err := Origin(conf, tt.req)
if err != nil {
t.Error(err)
continue
}
if !reflect.DeepEqual(origin, tt.origin) {
t.Errorf("#%d: got origin %v; want %v", i, origin, tt.origin)
continue
}
}
}
func TestCtrlAndData(t *testing.T) {
once.Do(startServer)
c, err := net.Dial("tcp", serverAddr)
if err != nil {
t.Fatal(err)
}
ws, err := NewClient(newConfig(t, "/ctrldata"), c)
if err != nil {
t.Fatal(err)
}
defer ws.Close()
h := &testCtrlAndDataHandler{hybiFrameHandler: hybiFrameHandler{conn: ws}}
ws.frameHandler = h
b := make([]byte, 128)
for i := 0; i < 2; i++ {
data := []byte(fmt.Sprintf("#%d-DATA-FRAME-FROM-CLIENT", i))
if _, err := ws.Write(data); err != nil {
t.Fatalf("#%d: %v", i, err)
}
var ctrl []byte
if i%2 != 0 { // with or without payload
ctrl = []byte(fmt.Sprintf("#%d-CONTROL-FRAME-FROM-CLIENT", i))
}
if _, err := h.WritePing(ctrl); err != nil {
t.Fatalf("#%d: %v", i, err)
}
n, err := ws.Read(b)
if err != nil {
t.Fatalf("#%d: %v", i, err)
}
if !bytes.Equal(b[:n], data) {
t.Fatalf("#%d: got %v; want %v", i, b[:n], data)
}
}
}