Merge pull request #14747 from eparis/update-golang-x-net

Update golang.org/x/net to fix godep reproducability problem
This commit is contained in:
Fabio Yeon 2015-09-29 11:18:55 -07:00
commit 26c51881ca
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,16 +64,19 @@ 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)
// if err != nil {
// return err
// }
// select { // select {
// case res := <-c:
// return res, nil
// case <-ctx.Done(): // case <-ctx.Done():
// return nil, ctx.Err() // return ctx.Err()
// case out <- v:
// }
// } // }
// } // }
// //
@ -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,8 +31,20 @@ 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
}
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 br := bufio.NewReader(strings.NewReader(`HTTP/1.1 101 Switching Protocols
Upgrade: websocket Upgrade: websocket
Connection: Upgrade Connection: Upgrade
@ -41,8 +53,8 @@ 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)
} }
@ -53,29 +65,27 @@ Sec-WebSocket-Protocol: chat
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==",
} }
err = hybiClientHandshake(config, br, bw) if err := hybiClientHandshake(&config, br, bw); err != nil {
if err != nil { t.Fatal("handshake", err)
t.Errorf("handshake failed: %v", err)
} }
req, err := http.ReadRequest(bufio.NewReader(b)) req, err := http.ReadRequest(bufio.NewReader(&b))
if err != nil { if err != nil {
t.Fatalf("read request: %v", err) t.Fatal("read request", err)
} }
if req.Method != "GET" { if req.Method != "GET" {
t.Errorf("request method expected GET, but got %q", req.Method) t.Errorf("request method expected GET, but got %s", req.Method)
} }
if req.URL.Path != "/chat" { if req.URL.Path != "/chat" {
t.Errorf("request path expected /chat, but got %q", req.URL.Path) t.Errorf("request path expected /chat, but got %s", req.URL.Path)
} }
if req.Proto != "HTTP/1.1" { if req.Proto != "HTTP/1.1" {
t.Errorf("request proto expected HTTP/1.1, but got %q", req.Proto) t.Errorf("request proto expected HTTP/1.1, but got %s", req.Proto)
} }
if req.Host != "server.example.com" { if req.Host != tt.host {
t.Errorf("request Host expected server.example.com, but got %v", req.Host) t.Errorf("request host expected %s, but got %s", tt.host, req.Host)
} }
var expectedHeader = map[string]string{ var expectedHeader = map[string]string{
"Connection": "Upgrade", "Connection": "Upgrade",
@ -87,7 +97,8 @@ Sec-WebSocket-Protocol: chat
} }
for k, v := range expectedHeader { for k, v := range expectedHeader {
if req.Header.Get(k) != v { if req.Header.Get(k) != v {
t.Errorf(fmt.Sprintf("%s expected %q but got %q", k, v, req.Header.Get(k))) t.Errorf("%s expected %s, but got %v", 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)
}
}
}