snapshot of https://github.com/gorilla/websocket.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
70
.circleci/config.yml
Normal file
70
.circleci/config.yml
Normal file
@@ -0,0 +1,70 @@
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
"test":
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
default: "latest"
|
||||
golint:
|
||||
type: boolean
|
||||
default: true
|
||||
modules:
|
||||
type: boolean
|
||||
default: true
|
||||
goproxy:
|
||||
type: string
|
||||
default: ""
|
||||
docker:
|
||||
- image: "cimg/go:<< parameters.version >>"
|
||||
working_directory: /home/circleci/project/go/src/github.com/gorilla/websocket
|
||||
environment:
|
||||
GO111MODULE: "on"
|
||||
GOPROXY: "<< parameters.goproxy >>"
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: "Print the Go version"
|
||||
command: >
|
||||
go version
|
||||
- run:
|
||||
name: "Fetch dependencies"
|
||||
command: >
|
||||
if [[ << parameters.modules >> = true ]]; then
|
||||
go mod download
|
||||
export GO111MODULE=on
|
||||
else
|
||||
go get -v ./...
|
||||
fi
|
||||
# Only run gofmt, vet & lint against the latest Go version
|
||||
- run:
|
||||
name: "Run golint"
|
||||
command: >
|
||||
if [ << parameters.version >> = "latest" ] && [ << parameters.golint >> = true ]; then
|
||||
go get -u golang.org/x/lint/golint
|
||||
golint ./...
|
||||
fi
|
||||
- run:
|
||||
name: "Run gofmt"
|
||||
command: >
|
||||
if [[ << parameters.version >> = "latest" ]]; then
|
||||
diff -u <(echo -n) <(gofmt -d -e .)
|
||||
fi
|
||||
- run:
|
||||
name: "Run go vet"
|
||||
command: >
|
||||
if [[ << parameters.version >> = "latest" ]]; then
|
||||
go vet -v ./...
|
||||
fi
|
||||
- run:
|
||||
name: "Run go test (+ race detector)"
|
||||
command: >
|
||||
go test -v -race ./...
|
||||
|
||||
workflows:
|
||||
tests:
|
||||
jobs:
|
||||
- test:
|
||||
matrix:
|
||||
parameters:
|
||||
version: ["1.22", "1.21", "1.20"]
|
||||
7
.github/release-drafter.yml
vendored
Normal file
7
.github/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Config for https://github.com/apps/release-drafter
|
||||
template: |
|
||||
|
||||
<summary of changes here>
|
||||
|
||||
## CHANGELOG
|
||||
$CHANGES
|
||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
|
||||
.idea/
|
||||
*.iml
|
||||
9
AUTHORS
Normal file
9
AUTHORS
Normal file
@@ -0,0 +1,9 @@
|
||||
# This is the official list of Gorilla WebSocket authors for copyright
|
||||
# purposes.
|
||||
#
|
||||
# Please keep the list sorted.
|
||||
|
||||
Gary Burd <gary@beagledreams.com>
|
||||
Google LLC (https://opensource.google.com/)
|
||||
Joachim Bauch <mail@joachim-bauch.de>
|
||||
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
32
README.md
Normal file
32
README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Gorilla WebSocket
|
||||
|
||||
[](https://godoc.org/github.com/gorilla/websocket)
|
||||
[](https://circleci.com/gh/gorilla/websocket)
|
||||
|
||||
Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
|
||||
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc)
|
||||
* [Chat example](https://github.com/gorilla/websocket/tree/main/examples/chat)
|
||||
* [Command example](https://github.com/gorilla/websocket/tree/main/examples/command)
|
||||
* [Client and server example](https://github.com/gorilla/websocket/tree/main/examples/echo)
|
||||
* [File watch example](https://github.com/gorilla/websocket/tree/main/examples/filewatch)
|
||||
|
||||
### Status
|
||||
|
||||
The Gorilla WebSocket package provides a complete and tested implementation of
|
||||
the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The
|
||||
package API is stable.
|
||||
|
||||
### Installation
|
||||
|
||||
go get github.com/gorilla/websocket
|
||||
|
||||
### Protocol Compliance
|
||||
|
||||
The Gorilla WebSocket package passes the server tests in the [Autobahn Test
|
||||
Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn
|
||||
subdirectory](https://github.com/gorilla/websocket/tree/main/examples/autobahn).
|
||||
517
client.go
Normal file
517
client.go
Normal file
@@ -0,0 +1,517 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrBadHandshake is returned when the server response to opening handshake is
|
||||
// invalid.
|
||||
var ErrBadHandshake = errors.New("websocket: bad handshake")
|
||||
|
||||
var errInvalidCompression = errors.New("websocket: invalid compression negotiation")
|
||||
|
||||
// NewClient creates a new client connection using the given net connection.
|
||||
// The URL u specifies the host and request URI. Use requestHeader to specify
|
||||
// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies
|
||||
// (Cookie). Use the response.Header to get the selected subprotocol
|
||||
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
|
||||
//
|
||||
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
|
||||
// non-nil *http.Response so that callers can handle redirects, authentication,
|
||||
// etc.
|
||||
//
|
||||
// Deprecated: Use Dialer instead.
|
||||
func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) {
|
||||
d := Dialer{
|
||||
ReadBufferSize: readBufSize,
|
||||
WriteBufferSize: writeBufSize,
|
||||
NetDial: func(net, addr string) (net.Conn, error) {
|
||||
return netConn, nil
|
||||
},
|
||||
}
|
||||
return d.Dial(u.String(), requestHeader)
|
||||
}
|
||||
|
||||
// A Dialer contains options for connecting to WebSocket server.
|
||||
//
|
||||
// It is safe to call Dialer's methods concurrently.
|
||||
type Dialer struct {
|
||||
// The following custom dial functions can be set to establish
|
||||
// connections to either the backend server or the proxy (if it
|
||||
// exists). The scheme of the dialed entity (either backend or
|
||||
// proxy) determines which custom dial function is selected:
|
||||
// either NetDialTLSContext for HTTPS or NetDialContext/NetDial
|
||||
// for HTTP. Since the "Proxy" function can determine the scheme
|
||||
// dynamically, it can make sense to set multiple custom dial
|
||||
// functions simultaneously.
|
||||
//
|
||||
// NetDial specifies the dial function for creating TCP connections. If
|
||||
// NetDial is nil, net.Dialer DialContext is used.
|
||||
// If "Proxy" field is also set, this function dials the proxy--not
|
||||
// the backend server.
|
||||
NetDial func(network, addr string) (net.Conn, error)
|
||||
|
||||
// NetDialContext specifies the dial function for creating TCP connections. If
|
||||
// NetDialContext is nil, NetDial is used.
|
||||
// If "Proxy" field is also set, this function dials the proxy--not
|
||||
// the backend server.
|
||||
NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// NetDialTLSContext specifies the dial function for creating TLS/TCP connections. If
|
||||
// NetDialTLSContext is nil, NetDialContext is used.
|
||||
// If NetDialTLSContext is set, Dial assumes the TLS handshake is done there and
|
||||
// TLSClientConfig is ignored.
|
||||
// If "Proxy" field is also set, this function dials the proxy (and performs
|
||||
// the TLS handshake with the proxy, ignoring TLSClientConfig). In this TLS proxy
|
||||
// dialing case the TLSClientConfig could still be necessary for TLS to the backend server.
|
||||
NetDialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// Proxy specifies a function to return a proxy for a given
|
||||
// Request. If the function returns a non-nil error, the
|
||||
// request is aborted with the provided error.
|
||||
// If Proxy is nil or returns a nil *URL, no proxy is used.
|
||||
Proxy func(*http.Request) (*url.URL, error)
|
||||
|
||||
// TLSClientConfig specifies the TLS configuration to use with tls.Client.
|
||||
// If nil, the default configuration is used.
|
||||
// If NetDialTLSContext is set, Dial assumes the TLS handshake
|
||||
// is done there and TLSClientConfig is ignored.
|
||||
TLSClientConfig *tls.Config
|
||||
|
||||
// HandshakeTimeout specifies the duration for the handshake to complete.
|
||||
HandshakeTimeout time.Duration
|
||||
|
||||
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
|
||||
// size is zero, then a useful default size is used. The I/O buffer sizes
|
||||
// do not limit the size of the messages that can be sent or received.
|
||||
ReadBufferSize, WriteBufferSize int
|
||||
|
||||
// WriteBufferPool is a pool of buffers for write operations. If the value
|
||||
// is not set, then write buffers are allocated to the connection for the
|
||||
// lifetime of the connection.
|
||||
//
|
||||
// A pool is most useful when the application has a modest volume of writes
|
||||
// across a large number of connections.
|
||||
//
|
||||
// Applications should use a single pool for each unique value of
|
||||
// WriteBufferSize.
|
||||
WriteBufferPool BufferPool
|
||||
|
||||
// Subprotocols specifies the client's requested subprotocols.
|
||||
Subprotocols []string
|
||||
|
||||
// EnableCompression specifies if the client should attempt to negotiate
|
||||
// per message compression (RFC 7692). Setting this value to true does not
|
||||
// guarantee that compression will be supported. Currently only "no context
|
||||
// takeover" modes are supported.
|
||||
EnableCompression bool
|
||||
|
||||
// Jar specifies the cookie jar.
|
||||
// If Jar is nil, cookies are not sent in requests and ignored
|
||||
// in responses.
|
||||
Jar http.CookieJar
|
||||
}
|
||||
|
||||
// Dial creates a new client connection by calling DialContext with a background context.
|
||||
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||
return d.DialContext(context.Background(), urlStr, requestHeader)
|
||||
}
|
||||
|
||||
var errMalformedURL = errors.New("malformed ws or wss URL")
|
||||
|
||||
func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
|
||||
hostPort = u.Host
|
||||
hostNoPort = u.Host
|
||||
if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") {
|
||||
hostNoPort = hostNoPort[:i]
|
||||
} else {
|
||||
switch u.Scheme {
|
||||
case "wss":
|
||||
hostPort += ":443"
|
||||
case "https":
|
||||
hostPort += ":443"
|
||||
default:
|
||||
hostPort += ":80"
|
||||
}
|
||||
}
|
||||
return hostPort, hostNoPort
|
||||
}
|
||||
|
||||
// DefaultDialer is a dialer with all fields set to the default values.
|
||||
var DefaultDialer = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
HandshakeTimeout: 45 * time.Second,
|
||||
}
|
||||
|
||||
// nilDialer is dialer to use when receiver is nil.
|
||||
var nilDialer = *DefaultDialer
|
||||
|
||||
// DialContext creates a new client connection. Use requestHeader to specify the
|
||||
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
|
||||
// Use the response.Header to get the selected subprotocol
|
||||
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
|
||||
//
|
||||
// The context will be used in the request and in the Dialer.
|
||||
//
|
||||
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
|
||||
// non-nil *http.Response so that callers can handle redirects, authentication,
|
||||
// etcetera. The response body may not contain the entire response and does not
|
||||
// need to be closed by the application.
|
||||
func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||
if d == nil {
|
||||
d = &nilDialer
|
||||
}
|
||||
|
||||
challengeKey, err := generateChallengeKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "ws":
|
||||
u.Scheme = "http"
|
||||
case "wss":
|
||||
u.Scheme = "https"
|
||||
default:
|
||||
return nil, nil, errMalformedURL
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
// User name and password are not allowed in websocket URIs.
|
||||
return nil, nil, errMalformedURL
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: u,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: make(http.Header),
|
||||
Host: u.Host,
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
// Set the cookies present in the cookie jar of the dialer
|
||||
if d.Jar != nil {
|
||||
for _, cookie := range d.Jar.Cookies(u) {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the request headers using the capitalization for names and values in
|
||||
// RFC examples. Although the capitalization shouldn't matter, there are
|
||||
// servers that depend on it. The Header.Set method is not used because the
|
||||
// method canonicalizes the header names.
|
||||
req.Header["Upgrade"] = []string{"websocket"}
|
||||
req.Header["Connection"] = []string{"Upgrade"}
|
||||
req.Header["Sec-WebSocket-Key"] = []string{challengeKey}
|
||||
req.Header["Sec-WebSocket-Version"] = []string{"13"}
|
||||
if len(d.Subprotocols) > 0 {
|
||||
req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")}
|
||||
}
|
||||
for k, vs := range requestHeader {
|
||||
switch {
|
||||
case k == "Host":
|
||||
if len(vs) > 0 {
|
||||
req.Host = vs[0]
|
||||
}
|
||||
case k == "Upgrade" ||
|
||||
k == "Connection" ||
|
||||
k == "Sec-Websocket-Key" ||
|
||||
k == "Sec-Websocket-Version" ||
|
||||
k == "Sec-Websocket-Extensions" ||
|
||||
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
|
||||
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
|
||||
case k == "Sec-Websocket-Protocol":
|
||||
req.Header["Sec-WebSocket-Protocol"] = vs
|
||||
default:
|
||||
req.Header[k] = vs
|
||||
}
|
||||
}
|
||||
|
||||
if d.EnableCompression {
|
||||
req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"}
|
||||
}
|
||||
|
||||
if d.HandshakeTimeout != 0 {
|
||||
var cancel func()
|
||||
ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
var proxyURL *url.URL
|
||||
if d.Proxy != nil {
|
||||
proxyURL, err = d.Proxy(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
netDial, err := d.netDialFn(ctx, proxyURL, u)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
hostPort, hostNoPort := hostPortNoPort(u)
|
||||
trace := httptrace.ContextClientTrace(ctx)
|
||||
if trace != nil && trace.GetConn != nil {
|
||||
trace.GetConn(hostPort)
|
||||
}
|
||||
|
||||
netConn, err := netDial(ctx, "tcp", hostPort)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if trace != nil && trace.GotConn != nil {
|
||||
trace.GotConn(httptrace.GotConnInfo{
|
||||
Conn: netConn,
|
||||
})
|
||||
}
|
||||
|
||||
// Close the network connection when returning an error. The variable
|
||||
// netConn is set to nil before the success return at the end of the
|
||||
// function.
|
||||
defer func() {
|
||||
if netConn != nil {
|
||||
// It's safe to ignore the error from Close() because this code is
|
||||
// only executed when returning a more important error to the
|
||||
// application.
|
||||
_ = netConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Do TLS handshake over established connection if a proxy exists.
|
||||
if proxyURL != nil && u.Scheme == "https" {
|
||||
|
||||
cfg := cloneTLSConfig(d.TLSClientConfig)
|
||||
if cfg.ServerName == "" {
|
||||
cfg.ServerName = hostNoPort
|
||||
}
|
||||
tlsConn := tls.Client(netConn, cfg)
|
||||
netConn = tlsConn
|
||||
|
||||
if trace != nil && trace.TLSHandshakeStart != nil {
|
||||
trace.TLSHandshakeStart()
|
||||
}
|
||||
err := doHandshake(ctx, tlsConn, cfg)
|
||||
if trace != nil && trace.TLSHandshakeDone != nil {
|
||||
trace.TLSHandshakeDone(tlsConn.ConnectionState(), err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil)
|
||||
|
||||
if err := req.Write(netConn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if trace != nil && trace.GotFirstResponseByte != nil {
|
||||
if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 {
|
||||
trace.GotFirstResponseByte()
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(conn.br, req)
|
||||
if err != nil {
|
||||
if d.TLSClientConfig != nil {
|
||||
for _, proto := range d.TLSClientConfig.NextProtos {
|
||||
if proto != "http/1.1" {
|
||||
return nil, nil, fmt.Errorf(
|
||||
"websocket: protocol %q was given but is not supported;"+
|
||||
"sharing tls.Config with net/http Transport can cause this error: %w",
|
||||
proto, err,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if d.Jar != nil {
|
||||
if rc := resp.Cookies(); len(rc) > 0 {
|
||||
d.Jar.SetCookies(u, rc)
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode != 101 ||
|
||||
!tokenListContainsValue(resp.Header, "Upgrade", "websocket") ||
|
||||
!tokenListContainsValue(resp.Header, "Connection", "upgrade") ||
|
||||
resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) {
|
||||
// Before closing the network connection on return from this
|
||||
// function, slurp up some of the response to aid application
|
||||
// debugging.
|
||||
buf := make([]byte, 1024)
|
||||
n, _ := io.ReadFull(resp.Body, buf)
|
||||
resp.Body = io.NopCloser(bytes.NewReader(buf[:n]))
|
||||
return nil, resp, ErrBadHandshake
|
||||
}
|
||||
|
||||
for _, ext := range parseExtensions(resp.Header) {
|
||||
if ext[""] != "permessage-deflate" {
|
||||
continue
|
||||
}
|
||||
_, snct := ext["server_no_context_takeover"]
|
||||
_, cnct := ext["client_no_context_takeover"]
|
||||
if !snct || !cnct {
|
||||
return nil, resp, errInvalidCompression
|
||||
}
|
||||
conn.newCompressionWriter = compressNoContextTakeover
|
||||
conn.newDecompressionReader = decompressNoContextTakeover
|
||||
break
|
||||
}
|
||||
|
||||
resp.Body = io.NopCloser(bytes.NewReader([]byte{}))
|
||||
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
|
||||
|
||||
if err := netConn.SetDeadline(time.Time{}); err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
// Success! Set netConn to nil to stop the deferred function above from
|
||||
// closing the network connection.
|
||||
netConn = nil
|
||||
|
||||
return conn, resp, nil
|
||||
}
|
||||
|
||||
// Returns the dial function to establish the connection to either the backend
|
||||
// server or the proxy (if it exists). If the dialed entity is HTTPS, then the
|
||||
// returned dial function *also* performs the TLS handshake to the dialed entity.
|
||||
// NOTE: If a proxy exists, it is possible for a second TLS handshake to be
|
||||
// necessary over the established connection.
|
||||
func (d *Dialer) netDialFn(ctx context.Context, proxyURL *url.URL, backendURL *url.URL) (netDialerFunc, error) {
|
||||
var netDial netDialerFunc
|
||||
if proxyURL != nil {
|
||||
netDial = d.netDialFromURL(proxyURL)
|
||||
} else {
|
||||
netDial = d.netDialFromURL(backendURL)
|
||||
}
|
||||
// If needed, wrap the dial function to set the connection deadline.
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
netDial = netDialWithDeadline(netDial, deadline)
|
||||
}
|
||||
// Proxy dialing is wrapped to implement CONNECT method and possibly proxy auth.
|
||||
if proxyURL != nil {
|
||||
return proxyFromURL(proxyURL, netDial)
|
||||
}
|
||||
return netDial, nil
|
||||
}
|
||||
|
||||
// Returns function to create the connection depending on the Dialer's
|
||||
// custom dialing functions and the passed URL of entity connecting to.
|
||||
func (d *Dialer) netDialFromURL(u *url.URL) netDialerFunc {
|
||||
var netDial netDialerFunc
|
||||
switch {
|
||||
case d.NetDialContext != nil:
|
||||
netDial = d.NetDialContext
|
||||
case d.NetDial != nil:
|
||||
netDial = func(ctx context.Context, net, addr string) (net.Conn, error) {
|
||||
return d.NetDial(net, addr)
|
||||
}
|
||||
default:
|
||||
netDial = (&net.Dialer{}).DialContext
|
||||
}
|
||||
// If dialed entity is HTTPS, then either use custom TLS dialing function (if exists)
|
||||
// or wrap the previously computed "netDial" to use TLS config for handshake.
|
||||
if u.Scheme == "https" {
|
||||
if d.NetDialTLSContext != nil {
|
||||
netDial = d.NetDialTLSContext
|
||||
} else {
|
||||
netDial = netDialWithTLSHandshake(netDial, d.TLSClientConfig, u)
|
||||
}
|
||||
}
|
||||
return netDial
|
||||
}
|
||||
|
||||
// Returns wrapped "netDial" function, performing TLS handshake after connecting.
|
||||
func netDialWithTLSHandshake(netDial netDialerFunc, tlsConfig *tls.Config, u *url.URL) netDialerFunc {
|
||||
return func(ctx context.Context, unused, addr string) (net.Conn, error) {
|
||||
hostPort, hostNoPort := hostPortNoPort(u)
|
||||
trace := httptrace.ContextClientTrace(ctx)
|
||||
if trace != nil && trace.GetConn != nil {
|
||||
trace.GetConn(hostPort)
|
||||
}
|
||||
// Creates TCP connection to addr using passed "netDial" function.
|
||||
conn, err := netDial(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := cloneTLSConfig(tlsConfig)
|
||||
if cfg.ServerName == "" {
|
||||
cfg.ServerName = hostNoPort
|
||||
}
|
||||
tlsConn := tls.Client(conn, cfg)
|
||||
// Do the TLS handshake using TLSConfig over the wrapped connection.
|
||||
if trace != nil && trace.TLSHandshakeStart != nil {
|
||||
trace.TLSHandshakeStart()
|
||||
}
|
||||
err = doHandshake(ctx, tlsConn, cfg)
|
||||
if trace != nil && trace.TLSHandshakeDone != nil {
|
||||
trace.TLSHandshakeDone(tlsConn.ConnectionState(), err)
|
||||
}
|
||||
if err != nil {
|
||||
tlsConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Returns wrapped "netDial" function, setting passed deadline.
|
||||
func netDialWithDeadline(netDial netDialerFunc, deadline time.Time) netDialerFunc {
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
c, err := netDial(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = c.SetDeadline(deadline)
|
||||
if err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
|
||||
if cfg == nil {
|
||||
return &tls.Config{}
|
||||
}
|
||||
return cfg.Clone()
|
||||
}
|
||||
|
||||
func doHandshake(ctx context.Context, tlsConn *tls.Conn, cfg *tls.Config) error {
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if !cfg.InsecureSkipVerify {
|
||||
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
911
client_proxy_server_test.go
Normal file
911
client_proxy_server_test.go
Normal file
@@ -0,0 +1,911 @@
|
||||
// Copyright 2025 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// These test cases use a websocket client (Dialer)/proxy/websocket server (Upgrader)
|
||||
// to validate the cases where a proxy is an intermediary between a websocket client
|
||||
// and server. The test cases usually 1) create a websocket server which echoes any
|
||||
// data received back to the client, 2) a basic duplex streaming proxy, and 3) a
|
||||
// websocket client which sends random data to the server through the proxy,
|
||||
// validating any subsequent data received is the same as the data sent. The various
|
||||
// permutations include the proxy and backend schemes (HTTP or HTTPS), as well as
|
||||
// the custom dial functions (e.g NetDialContext, NetDial) set on the Dialer.
|
||||
|
||||
const (
|
||||
subprotocolV1 = "subprotocol-version-1"
|
||||
subprotocolV2 = "subprotocol-version-2"
|
||||
)
|
||||
|
||||
// Permutation 1
|
||||
//
|
||||
// Backend: HTTP
|
||||
// Proxy: HTTP
|
||||
func TestHTTPProxyAndBackend(t *testing.T) {
|
||||
websocketTLS := false
|
||||
proxyTLS := false
|
||||
// Start the websocket server, which echoes data back to sender.
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
// Start the proxy server.
|
||||
proxyServer, proxyServerURL, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
// Dial the websocket server through the proxy server.
|
||||
dialer := Dialer{
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
wsClient, _, err := dialer.Dial(websocketURL.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial error: %v", err)
|
||||
}
|
||||
// Send, receive, and validate random data over websocket connection.
|
||||
sendReceiveData(t, wsClient)
|
||||
// Validate the proxy server was called.
|
||||
if e, a := int64(1), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy not called")
|
||||
}
|
||||
}
|
||||
|
||||
// Permutation 2
|
||||
//
|
||||
// Backend: HTTP
|
||||
// Proxy: HTTP
|
||||
// DialFn: NetDial (dials proxy)
|
||||
func TestHTTPProxyWithNetDial(t *testing.T) {
|
||||
websocketTLS := false
|
||||
proxyTLS := false
|
||||
// Start the websocket server, which echoes data back to sender.
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
// Start the proxy server.
|
||||
proxyServer, proxyServerURL, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
// Dial the websocket server through the proxy server.
|
||||
var netDialCalled atomic.Int64
|
||||
dialer := Dialer{
|
||||
NetDial: func(network, addr string) (net.Conn, error) {
|
||||
netDialCalled.Add(1)
|
||||
return (&net.Dialer{}).DialContext(context.Background(), network, addr)
|
||||
},
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
wsClient, _, err := dialer.Dial(websocketURL.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial error: %v", err)
|
||||
}
|
||||
// Send, receive, and validate random data over websocket connection.
|
||||
sendReceiveData(t, wsClient)
|
||||
if e, a := int64(1), netDialCalled.Load(); e != a {
|
||||
t.Errorf("netDial not called")
|
||||
}
|
||||
// Validate the proxy server was called.
|
||||
if e, a := int64(1), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy not called")
|
||||
}
|
||||
}
|
||||
|
||||
// Permutation 3
|
||||
//
|
||||
// Backend: HTTP
|
||||
// Proxy: HTTP
|
||||
// DialFn: NetDialContext (dials proxy)
|
||||
func TestHTTPProxyWithNetDialContext(t *testing.T) {
|
||||
websocketTLS := false
|
||||
proxyTLS := false
|
||||
// Start the websocket server, which echoes data back to sender.
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
// Start the proxy server.
|
||||
proxyServer, proxyServerURL, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
// Dial the websocket server through the proxy server.
|
||||
var netDialCalled atomic.Int64
|
||||
dialer := Dialer{
|
||||
NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
netDialCalled.Add(1)
|
||||
return (&net.Dialer{}).DialContext(ctx, network, addr)
|
||||
},
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
wsClient, _, err := dialer.Dial(websocketURL.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial error: %v", err)
|
||||
}
|
||||
// Send, receive, and validate random data over websocket connection.
|
||||
sendReceiveData(t, wsClient)
|
||||
if e, a := int64(1), netDialCalled.Load(); e != a {
|
||||
t.Errorf("netDial not called")
|
||||
}
|
||||
// Validate the proxy server was called.
|
||||
if e, a := int64(1), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy not called")
|
||||
}
|
||||
}
|
||||
|
||||
// Permutation 4
|
||||
//
|
||||
// Backend: HTTPS
|
||||
// Proxy: HTTP
|
||||
// DialFn: NetDialTLSConfig (set but *ignored*)
|
||||
// TLS Config: set (used for backend TLS)
|
||||
func TestHTTPProxyWithHTTPSBackend(t *testing.T) {
|
||||
websocketTLS := true
|
||||
proxyTLS := false
|
||||
// Start the websocket server, which echoes data back to sender.
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
// Start the proxy server.
|
||||
proxyServer, proxyServerURL, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
var netDialTLSCalled atomic.Int64
|
||||
dialer := Dialer{
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
// This function should be ignored, because an HTTP proxy exists
|
||||
// and the backend TLS handshake should use TLSClientConfig.
|
||||
NetDialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
netDialTLSCalled.Add(1)
|
||||
return (&net.Dialer{}).DialContext(ctx, network, addr)
|
||||
},
|
||||
// Used for the backend server TLS handshake.
|
||||
TLSClientConfig: tlsConfig(websocketTLS, proxyTLS),
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
wsClient, _, err := dialer.Dial(websocketURL.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial error: %v", err)
|
||||
}
|
||||
// Send, receive, and validate random data over websocket connection.
|
||||
sendReceiveData(t, wsClient)
|
||||
if numTLSDials := netDialTLSCalled.Load(); numTLSDials > 0 {
|
||||
t.Errorf("NetDialTLS should have been ignored")
|
||||
}
|
||||
// Validate the proxy server was called.
|
||||
if e, a := int64(1), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy not called")
|
||||
}
|
||||
}
|
||||
|
||||
// Permutation 5
|
||||
//
|
||||
// Backend: HTTPS
|
||||
// Proxy: HTTPS
|
||||
// TLS Config: set (used for both proxy and backend TLS)
|
||||
func TestHTTPSProxyAndBackend(t *testing.T) {
|
||||
websocketTLS := true
|
||||
proxyTLS := true
|
||||
// Start the websocket server, which echoes data back to sender.
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
// Start the proxy server.
|
||||
proxyServer, proxyServerURL, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
dialer := Dialer{
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
TLSClientConfig: tlsConfig(websocketTLS, proxyTLS),
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
wsClient, _, err := dialer.Dial(websocketURL.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial error: %v", err)
|
||||
}
|
||||
// Send, receive, and validate random data over websocket connection.
|
||||
sendReceiveData(t, wsClient)
|
||||
// Validate the proxy server was called.
|
||||
if e, a := int64(1), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy not called")
|
||||
}
|
||||
}
|
||||
|
||||
// Permutation 6
|
||||
//
|
||||
// Backend: HTTPS
|
||||
// Proxy: HTTPS
|
||||
// DialFn: NetDial (used to dial proxy)
|
||||
// TLS Config: set (used for both proxy and backend TLS)
|
||||
func TestHTTPSProxyUsingNetDial(t *testing.T) {
|
||||
websocketTLS := true
|
||||
proxyTLS := true
|
||||
// Start the websocket server, which echoes data back to sender.
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
// Start the proxy server.
|
||||
proxyServer, proxyServerURL, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
var netDialCalled atomic.Int64
|
||||
dialer := Dialer{
|
||||
NetDial: func(network, addr string) (net.Conn, error) {
|
||||
netDialCalled.Add(1)
|
||||
return (&net.Dialer{}).DialContext(context.Background(), network, addr)
|
||||
},
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
TLSClientConfig: tlsConfig(websocketTLS, proxyTLS),
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
wsClient, _, err := dialer.Dial(websocketURL.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial error: %v", err)
|
||||
}
|
||||
// Send, receive, and validate random data over websocket connection.
|
||||
sendReceiveData(t, wsClient)
|
||||
if e, a := int64(1), netDialCalled.Load(); e != a {
|
||||
t.Errorf("netDial not called")
|
||||
}
|
||||
// Validate the proxy server was called.
|
||||
if e, a := int64(1), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy not called")
|
||||
}
|
||||
}
|
||||
|
||||
// Permutation 7
|
||||
//
|
||||
// Backend: HTTPS
|
||||
// Proxy: HTTPS
|
||||
// DialFn: NetDialContext (used to dial proxy)
|
||||
// TLS Config: set (used for both proxy and backend TLS)
|
||||
func TestHTTPSProxyUsingNetDialContext(t *testing.T) {
|
||||
websocketTLS := true
|
||||
proxyTLS := true
|
||||
// Start the websocket server, which echoes data back to sender.
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
// Start the proxy server.
|
||||
proxyServer, proxyServerURL, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
var netDialCalled atomic.Int64
|
||||
dialer := Dialer{
|
||||
NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
netDialCalled.Add(1)
|
||||
return (&net.Dialer{}).DialContext(ctx, network, addr)
|
||||
},
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
TLSClientConfig: tlsConfig(websocketTLS, proxyTLS),
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
wsClient, _, err := dialer.Dial(websocketURL.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial error: %v", err)
|
||||
}
|
||||
// Send, receive, and validate random data over websocket connection.
|
||||
sendReceiveData(t, wsClient)
|
||||
if e, a := int64(1), netDialCalled.Load(); e != a {
|
||||
t.Errorf("netDial not called")
|
||||
}
|
||||
// Validate the proxy server was called.
|
||||
if e, a := int64(1), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy not called")
|
||||
}
|
||||
}
|
||||
|
||||
// Permutation 8
|
||||
//
|
||||
// Backend: HTTPS
|
||||
// Proxy: HTTPS
|
||||
// DialFn: NetDialTLSContext (used for proxy TLS)
|
||||
// TLS Config: set (used for backend TLS)
|
||||
func TestHTTPSProxyUsingNetDialTLSContext(t *testing.T) {
|
||||
websocketTLS := true
|
||||
proxyTLS := true
|
||||
// Start the websocket server, which echoes data back to sender.
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
// Start the proxy server.
|
||||
proxyServer, proxyServerURL, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
// Configure the proxy dialing function which dials the proxy and
|
||||
// performs the TLS handshake.
|
||||
var proxyDialCalled atomic.Int64
|
||||
proxyCerts := x509.NewCertPool()
|
||||
proxyCerts.AppendCertsFromPEM(proxyServerCert)
|
||||
proxyTLSConfig := &tls.Config{RootCAs: proxyCerts}
|
||||
proxyDial := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
proxyDialCalled.Add(1)
|
||||
return tls.Dial(network, addr, proxyTLSConfig)
|
||||
}
|
||||
// Configure the backend webscocket TLS configuration (handshake occurs
|
||||
// over the previously created proxy connection).
|
||||
websocketCerts := x509.NewCertPool()
|
||||
websocketCerts.AppendCertsFromPEM(websocketServerCert)
|
||||
websocketTLSConfig := &tls.Config{RootCAs: websocketCerts}
|
||||
dialer := Dialer{
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
// Dial and TLS handshake function to proxy.
|
||||
NetDialTLSContext: proxyDial,
|
||||
// Used for second TLS handshake to backend server over previously
|
||||
// established proxy connection.
|
||||
TLSClientConfig: websocketTLSConfig,
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
wsClient, _, err := dialer.Dial(websocketURL.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial error: %v", err)
|
||||
}
|
||||
// Send, receive, and validate random data over websocket connection.
|
||||
sendReceiveData(t, wsClient)
|
||||
if e, a := int64(1), proxyDialCalled.Load(); e != a {
|
||||
t.Errorf("netDial not called")
|
||||
}
|
||||
// Validate the proxy server was called.
|
||||
if e, a := int64(1), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy not called")
|
||||
}
|
||||
}
|
||||
|
||||
// Permutation 9
|
||||
//
|
||||
// Backend: HTTP
|
||||
// Proxy: HTTPS
|
||||
// TLS Config: set (used for proxy TLS)
|
||||
func TestHTTPSProxyHTTPBackend(t *testing.T) {
|
||||
websocketTLS := false
|
||||
proxyTLS := true
|
||||
// Start the websocket server, which echoes data back to sender.
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
// Start the proxy server.
|
||||
proxyServer, proxyServerURL, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
dialer := Dialer{
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
TLSClientConfig: tlsConfig(websocketTLS, proxyTLS),
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
wsClient, _, err := dialer.Dial(websocketURL.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial error: %v", err)
|
||||
}
|
||||
// Send, receive, and validate random data over websocket connection.
|
||||
sendReceiveData(t, wsClient)
|
||||
// Validate the proxy server was called.
|
||||
if e, a := int64(1), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy not called")
|
||||
}
|
||||
}
|
||||
|
||||
// Permutation 10
|
||||
//
|
||||
// Backend: HTTP
|
||||
// Proxy: HTTPS
|
||||
// DialFn: NetDialTLSContext (used for proxy TLS)
|
||||
// TLS Config: set (ignored)
|
||||
func TestHTTPSProxyUsingNetDialTLSContextWithHTTPBackend(t *testing.T) {
|
||||
websocketTLS := false
|
||||
proxyTLS := true
|
||||
// Start the websocket server, which echoes data back to sender.
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
// Start the proxy server.
|
||||
proxyServer, proxyServerURL, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
var proxyDialCalled atomic.Int64
|
||||
dialer := Dialer{
|
||||
NetDialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
proxyDialCalled.Add(1)
|
||||
return tls.Dial(network, addr, tlsConfig(websocketTLS, proxyTLS))
|
||||
},
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
TLSClientConfig: &tls.Config{}, // Misconfigured, but ignored.
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
wsClient, _, err := dialer.Dial(websocketURL.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial error: %v", err)
|
||||
}
|
||||
// Send, receive, and validate random data over websocket connection.
|
||||
sendReceiveData(t, wsClient)
|
||||
if e, a := int64(1), proxyDialCalled.Load(); e != a {
|
||||
t.Errorf("netDial not called")
|
||||
}
|
||||
// Validate the proxy server was called.
|
||||
if e, a := int64(1), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSValidationErrors(t *testing.T) {
|
||||
// Both websocket and proxy servers are started with TLS.
|
||||
websocketTLS := true
|
||||
proxyTLS := true
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
proxyServer, proxyServerURL, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
// Dialer without proxy CA cert fails TLS verification.
|
||||
tlsError := "tls: failed to verify certificate"
|
||||
dialer := Dialer{
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
TLSClientConfig: tlsConfig(true, false),
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
_, _, err = dialer.Dial(websocketURL.String(), nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected proxy TLS verification error did not arrive")
|
||||
} else if !strings.Contains(err.Error(), tlsError) {
|
||||
t.Errorf("expected proxy TLS error (%s), got (%s)", err.Error(), tlsError)
|
||||
}
|
||||
// Validate the proxy handler was *NOT* called (because proxy
|
||||
// server TLS validation failed).
|
||||
if e, a := int64(0), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy should not have been called")
|
||||
}
|
||||
// Dialer without websocket CA cert fails TLS verification.
|
||||
dialer = Dialer{
|
||||
Proxy: http.ProxyURL(proxyServerURL),
|
||||
TLSClientConfig: tlsConfig(false, true),
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
_, _, err = dialer.Dial(websocketURL.String(), nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected websocket TLS verification error did not arrive")
|
||||
} else if !strings.Contains(err.Error(), tlsError) {
|
||||
t.Errorf("expected websocket TLS error (%s), got (%s)", err.Error(), tlsError)
|
||||
}
|
||||
// Validate the proxy server *was* called (but subsequent
|
||||
// websocket server failed TLS validation).
|
||||
if e, a := int64(1), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy have been called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFnErrorIsPropagated(t *testing.T) {
|
||||
websocketServer, websocketURL, err := newWebsocketServer(false)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
// Create a Dialer where Proxy function always returns an error.
|
||||
proxyURLError := errors.New("proxy URL generation error")
|
||||
dialer := Dialer{
|
||||
Proxy: func(r *http.Request) (*url.URL, error) {
|
||||
return nil, proxyURLError
|
||||
},
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
// Proxy URL generation error should halt request and be propagated.
|
||||
_, _, err = dialer.Dial(websocketURL.String(), nil)
|
||||
if err == nil {
|
||||
t.Fatalf("expected websocket dial error, received none")
|
||||
} else if !errors.Is(proxyURLError, err) {
|
||||
t.Fatalf("expected error (%s), got (%s)", proxyURLError, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFnNilMeansNoProxy(t *testing.T) {
|
||||
// Both websocket and proxy servers are started.
|
||||
websocketTLS := false
|
||||
proxyTLS := false
|
||||
websocketServer, websocketURL, err := newWebsocketServer(websocketTLS)
|
||||
defer websocketServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting websocket server: %v", err)
|
||||
}
|
||||
proxyServer, _, err := newProxyServer(proxyTLS)
|
||||
defer proxyServer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error starting proxy server: %v", err)
|
||||
}
|
||||
// Dialer created with Proxy URL generation function returning nil
|
||||
// proxy URL, which continues with backend server connection without
|
||||
// proxying.
|
||||
dialer := Dialer{
|
||||
Proxy: func(r *http.Request) (*url.URL, error) {
|
||||
return nil, nil
|
||||
},
|
||||
Subprotocols: []string{subprotocolV1},
|
||||
}
|
||||
wsClient, _, err := dialer.Dial(websocketURL.String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("websocket dial error: %v", err)
|
||||
}
|
||||
sendReceiveData(t, wsClient)
|
||||
// Validate the proxy handler was *NOT* called (because proxy
|
||||
// URL generation returned nil).
|
||||
if e, a := int64(0), proxyServer.numCalls(); e != a {
|
||||
t.Errorf("proxy should not have been called")
|
||||
}
|
||||
}
|
||||
|
||||
// "counter" interface can be implemented by a server to keep track
|
||||
// of the number of times a handler was called, as well as "Close".
|
||||
type counter interface {
|
||||
increment()
|
||||
numCalls() int64
|
||||
closer
|
||||
}
|
||||
|
||||
type closer interface {
|
||||
Close()
|
||||
}
|
||||
|
||||
// testServer implements "counter" interface.
|
||||
type testServer struct {
|
||||
server *httptest.Server
|
||||
numHandled atomic.Int64
|
||||
}
|
||||
|
||||
func (ts *testServer) numCalls() int64 {
|
||||
return ts.numHandled.Load()
|
||||
}
|
||||
|
||||
func (ts *testServer) increment() {
|
||||
ts.numHandled.Add(1)
|
||||
}
|
||||
|
||||
func (ts *testServer) Close() {
|
||||
if ts.server != nil {
|
||||
ts.server.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// websocketEchoHandler upgrades the connection associated with the request, and
|
||||
// echoes binary messages read off the websocket connection back to the client.
|
||||
var websocketEchoHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
upgrader := Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // Accepting all requests
|
||||
},
|
||||
Subprotocols: []string{
|
||||
subprotocolV1,
|
||||
subprotocolV2,
|
||||
},
|
||||
}
|
||||
wsConn, err := upgrader.Upgrade(w, req, nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
defer wsConn.Close()
|
||||
for {
|
||||
writer, err := wsConn.NextWriter(BinaryMessage)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
messageType, reader, err := wsConn.NextReader()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if messageType != BinaryMessage {
|
||||
http.Error(w, "websocket reader not binary message type",
|
||||
http.StatusInternalServerError)
|
||||
}
|
||||
_, err = io.Copy(writer, reader)
|
||||
if err != nil {
|
||||
http.Error(w, "websocket server io copy error",
|
||||
http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Returns a test backend websocket server as well as the URL pointing
|
||||
// to the server, or an error if one occurred. Sets up a TLS endpoint
|
||||
// on the server if the passed "tlsServer" is true.
|
||||
// func newWebsocketServer(tlsServer bool) (*httptest.Server, *url.URL, error) {
|
||||
func newWebsocketServer(tlsServer bool) (closer, *url.URL, error) {
|
||||
// Start the websocket server, which echoes data back to sender.
|
||||
websocketServer := httptest.NewUnstartedServer(websocketEchoHandler)
|
||||
if tlsServer {
|
||||
websocketKeyPair, err := tls.X509KeyPair(websocketServerCert, websocketServerKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
websocketServer.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{websocketKeyPair},
|
||||
}
|
||||
websocketServer.StartTLS()
|
||||
} else {
|
||||
websocketServer.Start()
|
||||
}
|
||||
websocketURL, err := url.Parse(websocketServer.URL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if tlsServer {
|
||||
websocketURL.Scheme = "wss"
|
||||
} else {
|
||||
websocketURL.Scheme = "ws"
|
||||
}
|
||||
return websocketServer, websocketURL, nil
|
||||
}
|
||||
|
||||
// proxyHandler creates a full duplex streaming connection between the client
|
||||
// (hijacking the http request connection), and an "upstream" dialed connection
|
||||
// to the "Host". Creates two goroutines to copy between connections in each direction.
|
||||
var proxyHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
// Validate the CONNECT method.
|
||||
if req.Method != http.MethodConnect {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
// Dial upstream server.
|
||||
upstream, err := (&net.Dialer{}).DialContext(req.Context(), "tcp", req.URL.Host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer upstream.Close()
|
||||
// Return 200 OK to client.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// Hijack client connection.
|
||||
client, _, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
// Create duplex streaming between client and upstream connections.
|
||||
done := make(chan struct{}, 2)
|
||||
go func() {
|
||||
_, _ = io.Copy(upstream, client)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
go func() {
|
||||
_, _ = io.Copy(client, upstream)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
<-done
|
||||
})
|
||||
|
||||
// Returns a new test HTTP server, as well as the URL to that server, or
|
||||
// an error if one occurred. numProxyCalls keeps track of the number of
|
||||
// times the proxy handler was called with this server.
|
||||
func newProxyServer(tlsServer bool) (counter, *url.URL, error) {
|
||||
// Start the proxy server, keeping track of how many times the handler is called.
|
||||
ts := &testServer{}
|
||||
proxyServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ts.increment()
|
||||
proxyHandler.ServeHTTP(w, req)
|
||||
}))
|
||||
if tlsServer {
|
||||
proxyKeyPair, err := tls.X509KeyPair(proxyServerCert, proxyServerKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
proxyServer.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{proxyKeyPair},
|
||||
}
|
||||
proxyServer.StartTLS()
|
||||
} else {
|
||||
proxyServer.Start()
|
||||
}
|
||||
proxyURL, err := url.Parse(proxyServer.URL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return ts, proxyURL, nil
|
||||
}
|
||||
|
||||
// Returns the TLS config with the RootCAs cert pool set. If
|
||||
// neither websocket nor proxy server uses TLS, returns nil.
|
||||
func tlsConfig(websocketTLS bool, proxyTLS bool) *tls.Config {
|
||||
if !websocketTLS && !proxyTLS {
|
||||
return nil
|
||||
}
|
||||
certPool := x509.NewCertPool()
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: certPool,
|
||||
}
|
||||
if websocketTLS {
|
||||
tlsConfig.RootCAs.AppendCertsFromPEM(websocketServerCert)
|
||||
}
|
||||
if proxyTLS {
|
||||
tlsConfig.RootCAs.AppendCertsFromPEM(proxyServerCert)
|
||||
}
|
||||
return tlsConfig
|
||||
}
|
||||
|
||||
// Sends, receives, and validates random data sent and received
|
||||
// over the passed websocket connection.
|
||||
const randomDataSize = 128 * 1024
|
||||
|
||||
func sendReceiveData(t *testing.T, wsConn *Conn) {
|
||||
// Create the random data.
|
||||
randomData := make([]byte, randomDataSize)
|
||||
if _, err := rand.Read(randomData); err != nil {
|
||||
t.Errorf("unexpected error reading random data: %v", err)
|
||||
}
|
||||
// Send the random data.
|
||||
err := wsConn.WriteMessage(BinaryMessage, randomData)
|
||||
if err != nil {
|
||||
t.Errorf("websocket write error: %v", err)
|
||||
}
|
||||
// Read from the websocket connection, and validate the
|
||||
// read data is the same as the previously sent data.
|
||||
_, received, err := wsConn.ReadMessage()
|
||||
if !bytes.Equal(randomData, received) {
|
||||
t.Errorf("unexpected data received: %d bytes sent, %d bytes received",
|
||||
len(received), len(randomData))
|
||||
}
|
||||
}
|
||||
|
||||
// proxyServerCert was generated from crypto/tls/generate_cert.go with the following command:
|
||||
//
|
||||
// go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
|
||||
//
|
||||
// proxyServerCert is a self-signed.
|
||||
var proxyServerCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDGTCCAgGgAwIBAgIRALL5AZcefF4kkYV1SEG6YrMwDQYJKoZIhvcNAQELBQAw
|
||||
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
|
||||
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||
ADCCAQoCggEBALQ/FHcyVwdFHxARbbD2KBtDUT7Eni+8ioNdjtGcmtXqBv45EC1C
|
||||
JOqqGJTroFGJ6Q9kQIZ9FqH5IJR2fOOJD9kOTueG4Vt1JY1rj1Kbpjefu8XleZ5L
|
||||
SBwIWVnN/lEsEbuKmj7N2gLt5AH3zMZiBI1mg1u9Z5ZZHYbCiTpBrwsq6cTlvR9g
|
||||
dyo1YkM5hRESCzsrL0aUByoo0qRMD8ZsgANJwgsiO0/M6idbxDwv1BnGwGmRYvOE
|
||||
Hxpy3v0Jg7GJYrvnpnifJTs4nw91N5X9pXxR7FFzi/6HTYDWRljvTb0w6XciKYAz
|
||||
bWZ0+cJr5F7wB7ovlbm7HrQIR7z7EIIu2d8CAwEAAaNoMGYwDgYDVR0PAQH/BAQD
|
||||
AgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQFMAMBAf8wLgYDVR0R
|
||||
BCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZI
|
||||
hvcNAQELBQADggEBAFPPWopNEJtIA2VFAQcqN6uJK+JVFOnjGRoCrM6Xgzdm0wxY
|
||||
XCGjsxY5dl+V7KzdGqu858rCaq5osEBqypBpYAnS9C38VyCDA1vPS1PsN8SYv48z
|
||||
DyBwj+7R2qar0ADBhnhWxvYO9M72lN/wuCqFKYMeFSnJdQLv3AsrrHe9lYqOa36s
|
||||
8wxSwVTFTYXBzljPEnSaaJMPqFD8JXaZK1ryJPkO5OsCNQNGtatNiWAf3DcmwHAT
|
||||
MGYMzP0u4nw47aRz9shB8w+taPKHx2BVwE1m/yp3nHVioOjXqA1fwRQVGclCJSH1
|
||||
D2iq3hWVHRENgjTjANBPICLo9AZ4JfN6PH19mnU=
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
// proxyServerKey is the private key for proxyServerCert.
|
||||
var proxyServerKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAtD8UdzJXB0UfEBFtsPYoG0NRPsSeL7yKg12O0Zya1eoG/jkQ
|
||||
LUIk6qoYlOugUYnpD2RAhn0WofkglHZ844kP2Q5O54bhW3UljWuPUpumN5+7xeV5
|
||||
nktIHAhZWc3+USwRu4qaPs3aAu3kAffMxmIEjWaDW71nllkdhsKJOkGvCyrpxOW9
|
||||
H2B3KjViQzmFERILOysvRpQHKijSpEwPxmyAA0nCCyI7T8zqJ1vEPC/UGcbAaZFi
|
||||
84QfGnLe/QmDsYliu+emeJ8lOzifD3U3lf2lfFHsUXOL/odNgNZGWO9NvTDpdyIp
|
||||
gDNtZnT5wmvkXvAHui+VubsetAhHvPsQgi7Z3wIDAQABAoIBAGmw93IxjYCQ0ncc
|
||||
kSKMJNZfsdtJdaxuNRZ0nNNirhQzR2h403iGaZlEpmdkhzxozsWcto1l+gh+SdFk
|
||||
bTUK4MUZM8FlgO2dEqkLYh5BcMT7ICMZvSfJ4v21E5eqR68XVUqQKoQbNvQyxFk3
|
||||
EddeEGdNrkb0GDK8DKlBlzAW5ep4gjG85wSTjR+J+muUv3R0BgLBFSuQnIDM/IMB
|
||||
LWqsja/QbtB7yppe7jL5u8UCFdZG8BBKT9fcvFIu5PRLO3MO0uOI7LTc8+W1Xm23
|
||||
uv+j3SY0+v+6POjK0UlJFFi/wkSPTFIfrQO1qFBkTDQHhQ6q/7GnILYYOiGbIRg2
|
||||
NNuP52ECgYEAzXEoy50wSYh8xfFaBuxbm3ruuG2W49jgop7ZfoFrPWwOQKAZS441
|
||||
VIwV4+e5IcA6KkuYbtGSdTYqK1SMkgnUyD/VevwAqH5TJoEIGu0pDuKGwVuwqioZ
|
||||
frCIAV5GllKyUJ55VZNbRr2vY2fCsWbaCSCHETn6C16DNuTCe5C0JBECgYEA4JqY
|
||||
5GpNbMG8fOt4H7hU0Fbm2yd6SHJcQ3/9iimef7xG6ajxsYrIhg1ft+3IPHMjVI0+
|
||||
9brwHDnWg4bOOx/VO4VJBt6Dm/F33bndnZRkuIjfSNpLM51P+EnRdaFVHOJHwKqx
|
||||
uF69kihifCAG7YATgCveeXImzBUSyZUz9UrETu8CgYARNBimdFNG1RcdvEg9rC0/
|
||||
p9u1tfecvNySwZqU7WF9kz7eSonTueTdX521qAHowaAdSpdJMGODTTXaywm6cPhQ
|
||||
jIfj9JZZhbqQzt1O4+08Qdvm9TamCUB5S28YLjza+bHU7nBaqixKkDfPqzCyilpX
|
||||
yVGGL8SwjwmN3zop/sQXAQKBgC0JMsESQ6YcDsRpnrOVjYQc+LtW5iEitTdfsaID
|
||||
iGGKihmOI7B66IxgoCHMTws39wycKdSyADVYr5e97xpR3rrJlgQHmBIrz+Iow7Q2
|
||||
LiAGaec8xjl6QK/DdXmFuQBKqyKJ14rljFODP4QuE9WJid94bGqjpf3j99ltznZP
|
||||
4J8HAoGAJb4eb4lu4UGwifDzqfAPzLGCoi0fE1/hSx34lfuLcc1G+LEu9YDKoOVJ
|
||||
9suOh0b5K/bfEy9KrVMBBriduvdaERSD8S3pkIQaitIz0B029AbE4FLFf9lKQpP2
|
||||
KR8NJEkK99Vh/tew6jAMll70xFrE7aF8VLXJVE7w4sQzuvHxl9Q=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`)
|
||||
|
||||
// websocketServerCert is self-signed.
|
||||
var websocketServerCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDOTCCAiGgAwIBAgIQYSN1VY/favsLUo+B7gJ5tTANBgkqhkiG9w0BAQsFADAS
|
||||
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
|
||||
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEApBlintjkL1fO1Sk2pzNvl862CtTwU7/Jy6EZqWzI17wEbPn4sbSD
|
||||
bHhfDlPl2nmw3hVkc6LNK+eqzm2GX/ai4tgMiaH7kyyNit1K3g7y7GISMf9poWIa
|
||||
POJhid2wmhKHbEtHECSdQ5c/jEN1UVzB4go5LO7MEEVo9kyQ+yBqS6gISyFmfaT4
|
||||
qOsPJBir33bBpptSend1JSXaRTXqRa1p+oudw2ILa4U7KfuKK3emp21m5/HYAuSf
|
||||
CV4WqqDoDiBPMpsQ0kPEPugWZKFeF3qanmqFFvptYx+zJbOznWYY2D3idWsvcg6q
|
||||
VLPEB19oXaVBV0HXPFtObm5m1jCpl8FI1wIDAQABo4GIMIGFMA4GA1UdDwEB/wQE
|
||||
AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
|
||||
DgQWBBQcSkjqA9rgos1daegNj49BpRCA0jAuBgNVHREEJzAlggtleGFtcGxlLmNv
|
||||
bYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAnk9i
|
||||
9rogNTi9B1pn+Fbk3WALKdEjv/uyePsTnwdyvswVbeYbQweU9TrhYT2+eXbMA5kY
|
||||
7TaQm46idRqxCKMgc3Ip3DADJdm8cJX9p2ExU4fKdkPc1KD/J+4QHHx1W2Ml5S2o
|
||||
foOo6j1F0UdZP/rBj0UumEZp32qW+4DhVV/QQjUB8J0gaDC7yZBMdyMIeClR0RqE
|
||||
YfZdCJbQHqtTwBXN+imQUHPGmksYkRDpFRvw/4crpcMIE04mVVd99nOpFCQnK61t
|
||||
9US1y17VW1lYpkqlCS+rkcAtor4Z5naSf9/oLGCxEAwyW0pwHGO6MXtMxvB/JD20
|
||||
hJdlz1I7wlSfF4MiRQ==
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
// websocketServerKey is the private key for websocketServerCert.
|
||||
var websocketServerKey = []byte(`-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkGWKe2OQvV87V
|
||||
KTanM2+XzrYK1PBTv8nLoRmpbMjXvARs+fixtINseF8OU+XaebDeFWRzos0r56rO
|
||||
bYZf9qLi2AyJofuTLI2K3UreDvLsYhIx/2mhYho84mGJ3bCaEodsS0cQJJ1Dlz+M
|
||||
Q3VRXMHiCjks7swQRWj2TJD7IGpLqAhLIWZ9pPio6w8kGKvfdsGmm1J6d3UlJdpF
|
||||
NepFrWn6i53DYgtrhTsp+4ord6anbWbn8dgC5J8JXhaqoOgOIE8ymxDSQ8Q+6BZk
|
||||
oV4XepqeaoUW+m1jH7Mls7OdZhjYPeJ1ay9yDqpUs8QHX2hdpUFXQdc8W05ubmbW
|
||||
MKmXwUjXAgMBAAECggEAE6BkTDgH//rnkP/Ej/Y17Zkv6qxnMLe/4evwZB7PsrBu
|
||||
cxOUAWUOpvA1UO215bh87+2XvcDbUISnyC1kpKDyAGGeC5llER2DXE11VokWgtvZ
|
||||
Q0OXavw5w83A+WVGFFdiUmXP0l10CxEm7OwQjFz6D21GQ1qC65tG9NZZghTxbFTe
|
||||
iZKqgWqyHsaAWLOuDQbj1FTEBMFrY8f9RbclSh0luPZnzGc4BVI/t34jKPZBpH2N
|
||||
NCkr8aB7MMHGhrNZFHAu/KAvq8UBrDTX+O8ERMwcwQWB4nne2+GOTN0MdcAUc72i
|
||||
GryzIa8TgO+TpQOYoZ4NPnzFrsa+m3G2Tug3vbt62QKBgQDOPfM4/5/x/h/ggxQn
|
||||
aRvEOC+8ldeqEOS1VTGiuDKJMWXrNkG+d+AsxfNP4k0QVNrpEAZSYcf0gnS9Odcl
|
||||
luEsi/yPZDDnPg/cS+Z3336VKsggly7BWFs1Ct/9I+ZfSCl88TkVpIfeCBC34XEb
|
||||
0mFUq/RdLqXj/mVLbBfr+H8cEwKBgQDLsJUm8lkWFAPJ8UMto8xeUMGk44VukYwx
|
||||
+oI6KhplFntiI0C1Dd9wrxyCjySlJcc0NFt6IPN84d7pI9LQSbiKXQ1jMvsBzd4G
|
||||
EMtG8SHpIY/mMU+KzWLHYVFS0FA4PvXXvPRNLOXas7hbALZdLshVKd7aDlkQAb5C
|
||||
KWFHeIFwrQKBgA8r5Xl67HQrwoKMge4IQF+l1nUj/LJo/boNI1KaBDWtaZbs7dcq
|
||||
EFaa1TQ6LHsYEuZ0JFLpGIF3G0lUOOxt9fCF97VApIxON3J4LuMAkNo+RGyJUoos
|
||||
isETJLkFbAv0TgD/6bga21fM9hXgwqZOSpSk9ZvpM5DbBO6QbA4SwJ77AoGAX7h1
|
||||
/z14XAW/2hDE7xfAnLn6plA9jj5b0cjVlhvfF44/IVlLuUnxrPS9wyUdpXZhbMkG
|
||||
DBicFB3ZMVqiYTuju3ILLojwqGJkahlOTeJXe0VIaHbX2HS4bNXw76fxat07jsy/
|
||||
Sd1Fj0dR5YIqMRQhFNR+Y57Gf90x2cm0a2/X9GkCgYANawYx9bNfcX0HMVG7vktK
|
||||
6/80omnoBM0JUxA+V7DxS8kr9Cj2Y/kcS+VHb4yyoSkDgnsSdnCr1ZTctcj828MJ
|
||||
8AUwskAtEjPkHRXEgRRnEl2oJGD1TT5iwBNnuPAQDXwzkGCRYBnlfZNbILbOoSUz
|
||||
m+VDcqT5XzcRADa/TLlEXA==
|
||||
-----END PRIVATE KEY-----
|
||||
`)
|
||||
1245
client_server_test.go
Normal file
1245
client_server_test.go
Normal file
File diff suppressed because it is too large
Load Diff
32
client_test.go
Normal file
32
client_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2014 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var hostPortNoPortTests = []struct {
|
||||
u *url.URL
|
||||
hostPort, hostNoPort string
|
||||
}{
|
||||
{&url.URL{Scheme: "ws", Host: "example.com"}, "example.com:80", "example.com"},
|
||||
{&url.URL{Scheme: "wss", Host: "example.com"}, "example.com:443", "example.com"},
|
||||
{&url.URL{Scheme: "ws", Host: "example.com:7777"}, "example.com:7777", "example.com"},
|
||||
{&url.URL{Scheme: "wss", Host: "example.com:7777"}, "example.com:7777", "example.com"},
|
||||
}
|
||||
|
||||
func TestHostPortNoPort(t *testing.T) {
|
||||
for _, tt := range hostPortNoPortTests {
|
||||
hostPort, hostNoPort := hostPortNoPort(tt.u)
|
||||
if hostPort != tt.hostPort {
|
||||
t.Errorf("hostPortNoPort(%v) returned hostPort %q, want %q", tt.u, hostPort, tt.hostPort)
|
||||
}
|
||||
if hostNoPort != tt.hostNoPort {
|
||||
t.Errorf("hostPortNoPort(%v) returned hostNoPort %q, want %q", tt.u, hostNoPort, tt.hostNoPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
152
compression.go
Normal file
152
compression.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6
|
||||
maxCompressionLevel = flate.BestCompression
|
||||
defaultCompressionLevel = 1
|
||||
)
|
||||
|
||||
var (
|
||||
flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool
|
||||
flateReaderPool = sync.Pool{New: func() interface{} {
|
||||
return flate.NewReader(nil)
|
||||
}}
|
||||
)
|
||||
|
||||
func decompressNoContextTakeover(r io.Reader) io.ReadCloser {
|
||||
const tail =
|
||||
// Add four bytes as specified in RFC
|
||||
"\x00\x00\xff\xff" +
|
||||
// Add final block to squelch unexpected EOF error from flate reader.
|
||||
"\x01\x00\x00\xff\xff"
|
||||
|
||||
fr, _ := flateReaderPool.Get().(io.ReadCloser)
|
||||
mr := io.MultiReader(r, strings.NewReader(tail))
|
||||
if err := fr.(flate.Resetter).Reset(mr, nil); err != nil {
|
||||
// Reset never fails, but handle error in case that changes.
|
||||
fr = flate.NewReader(mr)
|
||||
}
|
||||
return &flateReadWrapper{fr}
|
||||
}
|
||||
|
||||
func isValidCompressionLevel(level int) bool {
|
||||
return minCompressionLevel <= level && level <= maxCompressionLevel
|
||||
}
|
||||
|
||||
func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser {
|
||||
p := &flateWriterPools[level-minCompressionLevel]
|
||||
tw := &truncWriter{w: w}
|
||||
fw, _ := p.Get().(*flate.Writer)
|
||||
if fw == nil {
|
||||
fw, _ = flate.NewWriter(tw, level)
|
||||
} else {
|
||||
fw.Reset(tw)
|
||||
}
|
||||
return &flateWriteWrapper{fw: fw, tw: tw, p: p}
|
||||
}
|
||||
|
||||
// truncWriter is an io.Writer that writes all but the last four bytes of the
|
||||
// stream to another io.Writer.
|
||||
type truncWriter struct {
|
||||
w io.WriteCloser
|
||||
n int
|
||||
p [4]byte
|
||||
}
|
||||
|
||||
func (w *truncWriter) Write(p []byte) (int, error) {
|
||||
n := 0
|
||||
|
||||
// fill buffer first for simplicity.
|
||||
if w.n < len(w.p) {
|
||||
n = copy(w.p[w.n:], p)
|
||||
p = p[n:]
|
||||
w.n += n
|
||||
if len(p) == 0 {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
m := len(p)
|
||||
if m > len(w.p) {
|
||||
m = len(w.p)
|
||||
}
|
||||
|
||||
if nn, err := w.w.Write(w.p[:m]); err != nil {
|
||||
return n + nn, err
|
||||
}
|
||||
|
||||
copy(w.p[:], w.p[m:])
|
||||
copy(w.p[len(w.p)-m:], p[len(p)-m:])
|
||||
nn, err := w.w.Write(p[:len(p)-m])
|
||||
return n + nn, err
|
||||
}
|
||||
|
||||
type flateWriteWrapper struct {
|
||||
fw *flate.Writer
|
||||
tw *truncWriter
|
||||
p *sync.Pool
|
||||
}
|
||||
|
||||
func (w *flateWriteWrapper) Write(p []byte) (int, error) {
|
||||
if w.fw == nil {
|
||||
return 0, errWriteClosed
|
||||
}
|
||||
return w.fw.Write(p)
|
||||
}
|
||||
|
||||
func (w *flateWriteWrapper) Close() error {
|
||||
if w.fw == nil {
|
||||
return errWriteClosed
|
||||
}
|
||||
err1 := w.fw.Flush()
|
||||
w.p.Put(w.fw)
|
||||
w.fw = nil
|
||||
if w.tw.p != [4]byte{0, 0, 0xff, 0xff} {
|
||||
return errors.New("websocket: internal error, unexpected bytes at end of flate stream")
|
||||
}
|
||||
err2 := w.tw.w.Close()
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
type flateReadWrapper struct {
|
||||
fr io.ReadCloser
|
||||
}
|
||||
|
||||
func (r *flateReadWrapper) Read(p []byte) (int, error) {
|
||||
if r.fr == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
n, err := r.fr.Read(p)
|
||||
if err == io.EOF {
|
||||
// Preemptively place the reader back in the pool. This helps with
|
||||
// scenarios where the application does not call NextReader() soon after
|
||||
// this final read.
|
||||
r.Close()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *flateReadWrapper) Close() error {
|
||||
if r.fr == nil {
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
err := r.fr.Close()
|
||||
flateReaderPool.Put(r.fr)
|
||||
r.fr = nil
|
||||
return err
|
||||
}
|
||||
79
compression_test.go
Normal file
79
compression_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type nopCloser struct{ io.Writer }
|
||||
|
||||
func (nopCloser) Close() error { return nil }
|
||||
|
||||
func TestTruncWriter(t *testing.T) {
|
||||
const data = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijlkmnopqrstuvwxyz987654321"
|
||||
for n := 1; n <= 10; n++ {
|
||||
var b bytes.Buffer
|
||||
w := &truncWriter{w: nopCloser{&b}}
|
||||
p := []byte(data)
|
||||
for len(p) > 0 {
|
||||
m := len(p)
|
||||
if m > n {
|
||||
m = n
|
||||
}
|
||||
_, _ = w.Write(p[:m])
|
||||
p = p[m:]
|
||||
}
|
||||
if b.String() != data[:len(data)-len(w.p)] {
|
||||
t.Errorf("%d: %q", n, b.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func textMessages(num int) [][]byte {
|
||||
messages := make([][]byte, num)
|
||||
for i := 0; i < num; i++ {
|
||||
msg := fmt.Sprintf("planet: %d, country: %d, city: %d, street: %d", i, i, i, i)
|
||||
messages[i] = []byte(msg)
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
func BenchmarkWriteNoCompression(b *testing.B) {
|
||||
w := io.Discard
|
||||
c := newTestConn(nil, w, false)
|
||||
messages := textMessages(100)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = c.WriteMessage(TextMessage, messages[i%len(messages)])
|
||||
}
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkWriteWithCompression(b *testing.B) {
|
||||
w := io.Discard
|
||||
c := newTestConn(nil, w, false)
|
||||
messages := textMessages(100)
|
||||
c.enableWriteCompression = true
|
||||
c.newCompressionWriter = compressNoContextTakeover
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = c.WriteMessage(TextMessage, messages[i%len(messages)])
|
||||
}
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func TestValidCompressionLevel(t *testing.T) {
|
||||
c := newTestConn(nil, nil, false)
|
||||
for _, level := range []int{minCompressionLevel - 1, maxCompressionLevel + 1} {
|
||||
if err := c.SetCompressionLevel(level); err == nil {
|
||||
t.Errorf("no error for level %d", level)
|
||||
}
|
||||
}
|
||||
for _, level := range []int{minCompressionLevel, maxCompressionLevel} {
|
||||
if err := c.SetCompressionLevel(level); err != nil {
|
||||
t.Errorf("error for level %d", level)
|
||||
}
|
||||
}
|
||||
}
|
||||
130
conn_broadcast_test.go
Normal file
130
conn_broadcast_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// broadcastBench allows to run broadcast benchmarks.
|
||||
// In every broadcast benchmark we create many connections, then send the same
|
||||
// message into every connection and wait for all writes complete. This emulates
|
||||
// an application where many connections listen to the same data - i.e. PUB/SUB
|
||||
// scenarios with many subscribers in one channel.
|
||||
type broadcastBench struct {
|
||||
w io.Writer
|
||||
closeCh chan struct{}
|
||||
doneCh chan struct{}
|
||||
count int32
|
||||
conns []*broadcastConn
|
||||
compression bool
|
||||
usePrepared bool
|
||||
}
|
||||
|
||||
type broadcastMessage struct {
|
||||
payload []byte
|
||||
prepared *PreparedMessage
|
||||
}
|
||||
|
||||
type broadcastConn struct {
|
||||
conn *Conn
|
||||
msgCh chan *broadcastMessage
|
||||
}
|
||||
|
||||
func newBroadcastConn(c *Conn) *broadcastConn {
|
||||
return &broadcastConn{
|
||||
conn: c,
|
||||
msgCh: make(chan *broadcastMessage, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func newBroadcastBench(usePrepared, compression bool) *broadcastBench {
|
||||
bench := &broadcastBench{
|
||||
w: io.Discard,
|
||||
doneCh: make(chan struct{}),
|
||||
closeCh: make(chan struct{}),
|
||||
usePrepared: usePrepared,
|
||||
compression: compression,
|
||||
}
|
||||
bench.makeConns(10000)
|
||||
return bench
|
||||
}
|
||||
|
||||
func (b *broadcastBench) makeConns(numConns int) {
|
||||
conns := make([]*broadcastConn, numConns)
|
||||
|
||||
for i := 0; i < numConns; i++ {
|
||||
c := newTestConn(nil, b.w, true)
|
||||
if b.compression {
|
||||
c.enableWriteCompression = true
|
||||
c.newCompressionWriter = compressNoContextTakeover
|
||||
}
|
||||
conns[i] = newBroadcastConn(c)
|
||||
go func(c *broadcastConn) {
|
||||
for {
|
||||
select {
|
||||
case msg := <-c.msgCh:
|
||||
if msg.prepared != nil {
|
||||
_ = c.conn.WritePreparedMessage(msg.prepared)
|
||||
} else {
|
||||
_ = c.conn.WriteMessage(TextMessage, msg.payload)
|
||||
}
|
||||
val := atomic.AddInt32(&b.count, 1)
|
||||
if val%int32(numConns) == 0 {
|
||||
b.doneCh <- struct{}{}
|
||||
}
|
||||
case <-b.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}(conns[i])
|
||||
}
|
||||
b.conns = conns
|
||||
}
|
||||
|
||||
func (b *broadcastBench) close() {
|
||||
close(b.closeCh)
|
||||
}
|
||||
|
||||
func (b *broadcastBench) broadcastOnce(msg *broadcastMessage) {
|
||||
for _, c := range b.conns {
|
||||
c.msgCh <- msg
|
||||
}
|
||||
<-b.doneCh
|
||||
}
|
||||
|
||||
func BenchmarkBroadcast(b *testing.B) {
|
||||
benchmarks := []struct {
|
||||
name string
|
||||
usePrepared bool
|
||||
compression bool
|
||||
}{
|
||||
{"NoCompression", false, false},
|
||||
{"Compression", false, true},
|
||||
{"NoCompressionPrepared", true, false},
|
||||
{"CompressionPrepared", true, true},
|
||||
}
|
||||
payload := textMessages(1)[0]
|
||||
for _, bm := range benchmarks {
|
||||
b.Run(bm.name, func(b *testing.B) {
|
||||
bench := newBroadcastBench(bm.usePrepared, bm.compression)
|
||||
defer bench.close()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
message := &broadcastMessage{
|
||||
payload: payload,
|
||||
}
|
||||
if bench.usePrepared {
|
||||
pm, _ := NewPreparedMessage(TextMessage, message.payload)
|
||||
message.prepared = pm
|
||||
}
|
||||
bench.broadcastOnce(message)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
})
|
||||
}
|
||||
}
|
||||
746
conn_test.go
Normal file
746
conn_test.go
Normal file
@@ -0,0 +1,746 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ net.Error = errWriteTimeout
|
||||
|
||||
type fakeNetConn struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (c fakeNetConn) Close() error { return nil }
|
||||
func (c fakeNetConn) LocalAddr() net.Addr { return localAddr }
|
||||
func (c fakeNetConn) RemoteAddr() net.Addr { return remoteAddr }
|
||||
func (c fakeNetConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (c fakeNetConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (c fakeNetConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
type fakeAddr int
|
||||
|
||||
var (
|
||||
localAddr = fakeAddr(1)
|
||||
remoteAddr = fakeAddr(2)
|
||||
)
|
||||
|
||||
func (a fakeAddr) Network() string {
|
||||
return "net"
|
||||
}
|
||||
|
||||
func (a fakeAddr) String() string {
|
||||
return "str"
|
||||
}
|
||||
|
||||
// newTestConn creates a connection backed by a fake network connection using
|
||||
// default values for buffering.
|
||||
func newTestConn(r io.Reader, w io.Writer, isServer bool) *Conn {
|
||||
return newConn(fakeNetConn{Reader: r, Writer: w}, isServer, 1024, 1024, nil, nil, nil)
|
||||
}
|
||||
|
||||
func TestFraming(t *testing.T) {
|
||||
frameSizes := []int{
|
||||
0, 1, 2, 124, 125, 126, 127, 128, 129, 65534, 65535,
|
||||
// 65536, 65537
|
||||
}
|
||||
var readChunkers = []struct {
|
||||
name string
|
||||
f func(io.Reader) io.Reader
|
||||
}{
|
||||
{"half", iotest.HalfReader},
|
||||
{"one", iotest.OneByteReader},
|
||||
{"asis", func(r io.Reader) io.Reader { return r }},
|
||||
}
|
||||
writeBuf := make([]byte, 65537)
|
||||
for i := range writeBuf {
|
||||
writeBuf[i] = byte(i)
|
||||
}
|
||||
var writers = []struct {
|
||||
name string
|
||||
f func(w io.Writer, n int) (int, error)
|
||||
}{
|
||||
{"iocopy", func(w io.Writer, n int) (int, error) {
|
||||
nn, err := io.Copy(w, bytes.NewReader(writeBuf[:n]))
|
||||
return int(nn), err
|
||||
}},
|
||||
{"write", func(w io.Writer, n int) (int, error) {
|
||||
return w.Write(writeBuf[:n])
|
||||
}},
|
||||
{"string", func(w io.Writer, n int) (int, error) {
|
||||
return io.WriteString(w, string(writeBuf[:n]))
|
||||
}},
|
||||
}
|
||||
|
||||
for _, compress := range []bool{false, true} {
|
||||
for _, isServer := range []bool{true, false} {
|
||||
for _, chunker := range readChunkers {
|
||||
|
||||
var connBuf bytes.Buffer
|
||||
wc := newTestConn(nil, &connBuf, isServer)
|
||||
rc := newTestConn(chunker.f(&connBuf), nil, !isServer)
|
||||
if compress {
|
||||
wc.newCompressionWriter = compressNoContextTakeover
|
||||
rc.newDecompressionReader = decompressNoContextTakeover
|
||||
}
|
||||
for _, n := range frameSizes {
|
||||
for _, writer := range writers {
|
||||
name := fmt.Sprintf("z:%v, s:%v, r:%s, n:%d w:%s", compress, isServer, chunker.name, n, writer.name)
|
||||
|
||||
w, err := wc.NextWriter(TextMessage)
|
||||
if err != nil {
|
||||
t.Errorf("%s: wc.NextWriter() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
nn, err := writer.f(w, n)
|
||||
if err != nil || nn != n {
|
||||
t.Errorf("%s: w.Write(writeBuf[:n]) returned %d, %v", name, nn, err)
|
||||
continue
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
t.Errorf("%s: w.Close() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
opCode, r, err := rc.NextReader()
|
||||
if err != nil || opCode != TextMessage {
|
||||
t.Errorf("%s: NextReader() returned %d, r, %v", name, opCode, err)
|
||||
continue
|
||||
}
|
||||
|
||||
t.Logf("frame size: %d", n)
|
||||
rbuf, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Errorf("%s: ReadFull() returned rbuf, %v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(rbuf) != n {
|
||||
t.Errorf("%s: len(rbuf) is %d, want %d", name, len(rbuf), n)
|
||||
continue
|
||||
}
|
||||
|
||||
for i, b := range rbuf {
|
||||
if byte(i) != b {
|
||||
t.Errorf("%s: bad byte at offset %d", name, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteControlDeadline(t *testing.T) {
|
||||
t.Parallel()
|
||||
message := []byte("hello")
|
||||
var connBuf bytes.Buffer
|
||||
c := newTestConn(nil, &connBuf, true)
|
||||
if err := c.WriteControl(PongMessage, message, time.Time{}); err != nil {
|
||||
t.Errorf("WriteControl(..., zero deadline) = %v, want nil", err)
|
||||
}
|
||||
if err := c.WriteControl(PongMessage, message, time.Now().Add(time.Second)); err != nil {
|
||||
t.Errorf("WriteControl(..., future deadline) = %v, want nil", err)
|
||||
}
|
||||
if err := c.WriteControl(PongMessage, message, time.Now().Add(-time.Second)); err == nil {
|
||||
t.Errorf("WriteControl(..., past deadline) = nil, want timeout error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrencyWriteControl(t *testing.T) {
|
||||
const message = "this is a ping/pong messsage"
|
||||
loop := 10
|
||||
workers := 10
|
||||
for i := 0; i < loop; i++ {
|
||||
var connBuf bytes.Buffer
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wc := newTestConn(nil, &connBuf, true)
|
||||
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := wc.WriteControl(PongMessage, []byte(message), time.Now().Add(time.Second)); err != nil {
|
||||
t.Errorf("concurrently wc.WriteControl() returned %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
wc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestControl(t *testing.T) {
|
||||
const message = "this is a ping/pong message"
|
||||
for _, isServer := range []bool{true, false} {
|
||||
for _, isWriteControl := range []bool{true, false} {
|
||||
name := fmt.Sprintf("s:%v, wc:%v", isServer, isWriteControl)
|
||||
var connBuf bytes.Buffer
|
||||
wc := newTestConn(nil, &connBuf, isServer)
|
||||
rc := newTestConn(&connBuf, nil, !isServer)
|
||||
if isWriteControl {
|
||||
_ = wc.WriteControl(PongMessage, []byte(message), time.Now().Add(time.Second))
|
||||
} else {
|
||||
w, err := wc.NextWriter(PongMessage)
|
||||
if err != nil {
|
||||
t.Errorf("%s: wc.NextWriter() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
if _, err := w.Write([]byte(message)); err != nil {
|
||||
t.Errorf("%s: w.Write() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Errorf("%s: w.Close() returned %v", name, err)
|
||||
continue
|
||||
}
|
||||
var actualMessage string
|
||||
rc.SetPongHandler(func(s string) error { actualMessage = s; return nil })
|
||||
_, _, _ = rc.NextReader()
|
||||
if actualMessage != message {
|
||||
t.Errorf("%s: pong=%q, want %q", name, actualMessage, message)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// simpleBufferPool is an implementation of BufferPool for TestWriteBufferPool.
|
||||
type simpleBufferPool struct {
|
||||
v interface{}
|
||||
}
|
||||
|
||||
func (p *simpleBufferPool) Get() interface{} {
|
||||
v := p.v
|
||||
p.v = nil
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *simpleBufferPool) Put(v interface{}) {
|
||||
p.v = v
|
||||
}
|
||||
|
||||
func TestWriteBufferPool(t *testing.T) {
|
||||
const message = "Now is the time for all good people to come to the aid of the party."
|
||||
|
||||
var buf bytes.Buffer
|
||||
var pool simpleBufferPool
|
||||
rc := newTestConn(&buf, nil, false)
|
||||
|
||||
// Specify writeBufferSize smaller than message size to ensure that pooling
|
||||
// works with fragmented messages.
|
||||
wc := newConn(fakeNetConn{Writer: &buf}, true, 1024, len(message)-1, &pool, nil, nil)
|
||||
|
||||
if wc.writeBuf != nil {
|
||||
t.Fatal("writeBuf not nil after create")
|
||||
}
|
||||
|
||||
// Part 1: test NextWriter/Write/Close
|
||||
|
||||
w, err := wc.NextWriter(TextMessage)
|
||||
if err != nil {
|
||||
t.Fatalf("wc.NextWriter() returned %v", err)
|
||||
}
|
||||
|
||||
if wc.writeBuf == nil {
|
||||
t.Fatal("writeBuf is nil after NextWriter")
|
||||
}
|
||||
|
||||
writeBufAddr := &wc.writeBuf[0]
|
||||
|
||||
if _, err := io.WriteString(w, message); err != nil {
|
||||
t.Fatalf("io.WriteString(w, message) returned %v", err)
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatalf("w.Close() returned %v", err)
|
||||
}
|
||||
|
||||
if wc.writeBuf != nil {
|
||||
t.Fatal("writeBuf not nil after w.Close()")
|
||||
}
|
||||
|
||||
if wpd, ok := pool.v.(writePoolData); !ok || len(wpd.buf) == 0 || &wpd.buf[0] != writeBufAddr {
|
||||
t.Fatal("writeBuf not returned to pool")
|
||||
}
|
||||
|
||||
opCode, p, err := rc.ReadMessage()
|
||||
if opCode != TextMessage || err != nil {
|
||||
t.Fatalf("ReadMessage() returned %d, p, %v", opCode, err)
|
||||
}
|
||||
|
||||
if s := string(p); s != message {
|
||||
t.Fatalf("message is %s, want %s", s, message)
|
||||
}
|
||||
|
||||
// Part 2: Test WriteMessage.
|
||||
|
||||
if err := wc.WriteMessage(TextMessage, []byte(message)); err != nil {
|
||||
t.Fatalf("wc.WriteMessage() returned %v", err)
|
||||
}
|
||||
|
||||
if wc.writeBuf != nil {
|
||||
t.Fatal("writeBuf not nil after wc.WriteMessage()")
|
||||
}
|
||||
|
||||
if wpd, ok := pool.v.(writePoolData); !ok || len(wpd.buf) == 0 || &wpd.buf[0] != writeBufAddr {
|
||||
t.Fatal("writeBuf not returned to pool after WriteMessage")
|
||||
}
|
||||
|
||||
opCode, p, err = rc.ReadMessage()
|
||||
if opCode != TextMessage || err != nil {
|
||||
t.Fatalf("ReadMessage() returned %d, p, %v", opCode, err)
|
||||
}
|
||||
|
||||
if s := string(p); s != message {
|
||||
t.Fatalf("message is %s, want %s", s, message)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriteBufferPoolSync ensures that *sync.Pool works as a buffer pool.
|
||||
func TestWriteBufferPoolSync(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
var pool sync.Pool
|
||||
wc := newConn(fakeNetConn{Writer: &buf}, true, 1024, 1024, &pool, nil, nil)
|
||||
rc := newTestConn(&buf, nil, false)
|
||||
|
||||
const message = "Hello World!"
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := wc.WriteMessage(TextMessage, []byte(message)); err != nil {
|
||||
t.Fatalf("wc.WriteMessage() returned %v", err)
|
||||
}
|
||||
opCode, p, err := rc.ReadMessage()
|
||||
if opCode != TextMessage || err != nil {
|
||||
t.Fatalf("ReadMessage() returned %d, p, %v", opCode, err)
|
||||
}
|
||||
if s := string(p); s != message {
|
||||
t.Fatalf("message is %s, want %s", s, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errorWriter is an io.Writer than returns an error on all writes.
|
||||
type errorWriter struct{}
|
||||
|
||||
func (ew errorWriter) Write(p []byte) (int, error) { return 0, errors.New("error") }
|
||||
|
||||
// TestWriteBufferPoolError ensures that buffer is returned to pool after error
|
||||
// on write.
|
||||
func TestWriteBufferPoolError(t *testing.T) {
|
||||
|
||||
// Part 1: Test NextWriter/Write/Close
|
||||
|
||||
var pool simpleBufferPool
|
||||
wc := newConn(fakeNetConn{Writer: errorWriter{}}, true, 1024, 1024, &pool, nil, nil)
|
||||
|
||||
w, err := wc.NextWriter(TextMessage)
|
||||
if err != nil {
|
||||
t.Fatalf("wc.NextWriter() returned %v", err)
|
||||
}
|
||||
|
||||
if wc.writeBuf == nil {
|
||||
t.Fatal("writeBuf is nil after NextWriter")
|
||||
}
|
||||
|
||||
writeBufAddr := &wc.writeBuf[0]
|
||||
|
||||
if _, err := io.WriteString(w, "Hello"); err != nil {
|
||||
t.Fatalf("io.WriteString(w, message) returned %v", err)
|
||||
}
|
||||
|
||||
if err := w.Close(); err == nil {
|
||||
t.Fatalf("w.Close() did not return error")
|
||||
}
|
||||
|
||||
if wpd, ok := pool.v.(writePoolData); !ok || len(wpd.buf) == 0 || &wpd.buf[0] != writeBufAddr {
|
||||
t.Fatal("writeBuf not returned to pool")
|
||||
}
|
||||
|
||||
// Part 2: Test WriteMessage
|
||||
|
||||
wc = newConn(fakeNetConn{Writer: errorWriter{}}, true, 1024, 1024, &pool, nil, nil)
|
||||
|
||||
if err := wc.WriteMessage(TextMessage, []byte("Hello")); err == nil {
|
||||
t.Fatalf("wc.WriteMessage did not return error")
|
||||
}
|
||||
|
||||
if wpd, ok := pool.v.(writePoolData); !ok || len(wpd.buf) == 0 || &wpd.buf[0] != writeBufAddr {
|
||||
t.Fatal("writeBuf not returned to pool")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseFrameBeforeFinalMessageFrame(t *testing.T) {
|
||||
const bufSize = 512
|
||||
|
||||
expectedErr := &CloseError{Code: CloseNormalClosure, Text: "hello"}
|
||||
|
||||
var b1, b2 bytes.Buffer
|
||||
wc := newConn(&fakeNetConn{Reader: nil, Writer: &b1}, false, 1024, bufSize, nil, nil, nil)
|
||||
rc := newTestConn(&b1, &b2, true)
|
||||
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
_, _ = w.Write(make([]byte, bufSize+bufSize/2))
|
||||
_ = wc.WriteControl(CloseMessage, FormatCloseMessage(expectedErr.Code, expectedErr.Text), time.Now().Add(10*time.Second))
|
||||
w.Close()
|
||||
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
_, err = io.Copy(io.Discard, r)
|
||||
if !reflect.DeepEqual(err, expectedErr) {
|
||||
t.Fatalf("io.Copy() returned %v, want %v", err, expectedErr)
|
||||
}
|
||||
_, _, err = rc.NextReader()
|
||||
if !reflect.DeepEqual(err, expectedErr) {
|
||||
t.Fatalf("NextReader() returned %v, want %v", err, expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEOFWithinFrame(t *testing.T) {
|
||||
const bufSize = 64
|
||||
|
||||
for n := 0; ; n++ {
|
||||
var b bytes.Buffer
|
||||
wc := newTestConn(nil, &b, false)
|
||||
rc := newTestConn(&b, nil, true)
|
||||
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
_, _ = w.Write(make([]byte, bufSize))
|
||||
w.Close()
|
||||
|
||||
if n >= b.Len() {
|
||||
break
|
||||
}
|
||||
b.Truncate(n)
|
||||
|
||||
op, r, err := rc.NextReader()
|
||||
if err == errUnexpectedEOF {
|
||||
continue
|
||||
}
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("%d: NextReader() returned %d, %v", n, op, err)
|
||||
}
|
||||
_, err = io.Copy(io.Discard, r)
|
||||
if err != errUnexpectedEOF {
|
||||
t.Fatalf("%d: io.Copy() returned %v, want %v", n, err, errUnexpectedEOF)
|
||||
}
|
||||
_, _, err = rc.NextReader()
|
||||
if err != errUnexpectedEOF {
|
||||
t.Fatalf("%d: NextReader() returned %v, want %v", n, err, errUnexpectedEOF)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEOFBeforeFinalFrame(t *testing.T) {
|
||||
const bufSize = 512
|
||||
|
||||
var b1, b2 bytes.Buffer
|
||||
wc := newConn(&fakeNetConn{Writer: &b1}, false, 1024, bufSize, nil, nil, nil)
|
||||
rc := newTestConn(&b1, &b2, true)
|
||||
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
_, _ = w.Write(make([]byte, bufSize+bufSize/2))
|
||||
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
_, err = io.Copy(io.Discard, r)
|
||||
if err != errUnexpectedEOF {
|
||||
t.Fatalf("io.Copy() returned %v, want %v", err, errUnexpectedEOF)
|
||||
}
|
||||
_, _, err = rc.NextReader()
|
||||
if err != errUnexpectedEOF {
|
||||
t.Fatalf("NextReader() returned %v, want %v", err, errUnexpectedEOF)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteAfterMessageWriterClose(t *testing.T) {
|
||||
wc := newTestConn(nil, &bytes.Buffer{}, false)
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
_, _ = io.WriteString(w, "hello")
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatalf("unexpected error closing message writer, %v", err)
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(w, "world"); err == nil {
|
||||
t.Fatalf("no error writing after close")
|
||||
}
|
||||
|
||||
w, _ = wc.NextWriter(BinaryMessage)
|
||||
_, _ = io.WriteString(w, "hello")
|
||||
|
||||
// close w by getting next writer
|
||||
_, err := wc.NextWriter(BinaryMessage)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error getting next writer, %v", err)
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(w, "world"); err == nil {
|
||||
t.Fatalf("no error writing after close")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLimit(t *testing.T) {
|
||||
t.Run("Test ReadLimit is enforced", func(t *testing.T) {
|
||||
const readLimit = 512
|
||||
message := make([]byte, readLimit+1)
|
||||
|
||||
var b1, b2 bytes.Buffer
|
||||
wc := newConn(&fakeNetConn{Writer: &b1}, false, 1024, readLimit-2, nil, nil, nil)
|
||||
rc := newTestConn(&b1, &b2, true)
|
||||
rc.SetReadLimit(readLimit)
|
||||
|
||||
// Send message at the limit with interleaved pong.
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
_, _ = w.Write(message[:readLimit-1])
|
||||
_ = wc.WriteControl(PongMessage, []byte("this is a pong"), time.Now().Add(10*time.Second))
|
||||
_, _ = w.Write(message[:1])
|
||||
w.Close()
|
||||
|
||||
// Send message larger than the limit.
|
||||
_ = wc.WriteMessage(BinaryMessage, message[:readLimit+1])
|
||||
|
||||
op, _, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("1: NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("2: NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
_, err = io.Copy(io.Discard, r)
|
||||
if err != ErrReadLimit {
|
||||
t.Fatalf("io.Copy() returned %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Test that ReadLimit cannot be overflowed", func(t *testing.T) {
|
||||
const readLimit = 1
|
||||
|
||||
var b1, b2 bytes.Buffer
|
||||
rc := newTestConn(&b1, &b2, true)
|
||||
rc.SetReadLimit(readLimit)
|
||||
|
||||
// First, send a non-final binary message
|
||||
b1.Write([]byte("\x02\x81"))
|
||||
|
||||
// Mask key
|
||||
b1.Write([]byte("\x00\x00\x00\x00"))
|
||||
|
||||
// First payload
|
||||
b1.Write([]byte("A"))
|
||||
|
||||
// Next, send a negative-length, non-final continuation frame
|
||||
b1.Write([]byte("\x00\xFF\x80\x00\x00\x00\x00\x00\x00\x00"))
|
||||
|
||||
// Mask key
|
||||
b1.Write([]byte("\x00\x00\x00\x00"))
|
||||
|
||||
// Next, send a too long, final continuation frame
|
||||
b1.Write([]byte("\x80\xFF\x00\x00\x00\x00\x00\x00\x00\x05"))
|
||||
|
||||
// Mask key
|
||||
b1.Write([]byte("\x00\x00\x00\x00"))
|
||||
|
||||
// Too-long payload
|
||||
b1.Write([]byte("BCDEF"))
|
||||
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("1: NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
|
||||
var buf [10]byte
|
||||
var read int
|
||||
n, err := r.Read(buf[:])
|
||||
if err != nil && err != ErrReadLimit {
|
||||
t.Fatalf("unexpected error testing read limit: %v", err)
|
||||
}
|
||||
read += n
|
||||
|
||||
n, err = r.Read(buf[:])
|
||||
if err != nil && err != ErrReadLimit {
|
||||
t.Fatalf("unexpected error testing read limit: %v", err)
|
||||
}
|
||||
read += n
|
||||
|
||||
if err == nil && read > readLimit {
|
||||
t.Fatalf("read limit exceeded: limit %d, read %d", readLimit, read)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddrs(t *testing.T) {
|
||||
c := newTestConn(nil, nil, true)
|
||||
if c.LocalAddr() != localAddr {
|
||||
t.Errorf("LocalAddr = %v, want %v", c.LocalAddr(), localAddr)
|
||||
}
|
||||
if c.RemoteAddr() != remoteAddr {
|
||||
t.Errorf("RemoteAddr = %v, want %v", c.RemoteAddr(), remoteAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeprecatedUnderlyingConn(t *testing.T) {
|
||||
var b1, b2 bytes.Buffer
|
||||
fc := fakeNetConn{Reader: &b1, Writer: &b2}
|
||||
c := newConn(fc, true, 1024, 1024, nil, nil, nil)
|
||||
ul := c.UnderlyingConn()
|
||||
if ul != fc {
|
||||
t.Fatalf("Underlying conn is not what it should be.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetConn(t *testing.T) {
|
||||
var b1, b2 bytes.Buffer
|
||||
fc := fakeNetConn{Reader: &b1, Writer: &b2}
|
||||
c := newConn(fc, true, 1024, 1024, nil, nil, nil)
|
||||
ul := c.NetConn()
|
||||
if ul != fc {
|
||||
t.Fatalf("Underlying conn is not what it should be.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufioReadBytes(t *testing.T) {
|
||||
// Test calling bufio.ReadBytes for value longer than read buffer size.
|
||||
|
||||
m := make([]byte, 512)
|
||||
m[len(m)-1] = '\n'
|
||||
|
||||
var b1, b2 bytes.Buffer
|
||||
wc := newConn(fakeNetConn{Writer: &b1}, false, len(m)+64, len(m)+64, nil, nil, nil)
|
||||
rc := newConn(fakeNetConn{Reader: &b1, Writer: &b2}, true, len(m)-64, len(m)-64, nil, nil, nil)
|
||||
|
||||
w, _ := wc.NextWriter(BinaryMessage)
|
||||
_, _ = w.Write(m)
|
||||
w.Close()
|
||||
|
||||
op, r, err := rc.NextReader()
|
||||
if op != BinaryMessage || err != nil {
|
||||
t.Fatalf("NextReader() returned %d, %v", op, err)
|
||||
}
|
||||
|
||||
br := bufio.NewReader(r)
|
||||
p, err := br.ReadBytes('\n')
|
||||
if err != nil {
|
||||
t.Fatalf("ReadBytes() returned %v", err)
|
||||
}
|
||||
if len(p) != len(m) {
|
||||
t.Fatalf("read returned %d bytes, want %d bytes", len(p), len(m))
|
||||
}
|
||||
}
|
||||
|
||||
var closeErrorTests = []struct {
|
||||
err error
|
||||
codes []int
|
||||
ok bool
|
||||
}{
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNormalClosure}, true},
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNoStatusReceived}, false},
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNoStatusReceived, CloseNormalClosure}, true},
|
||||
{errors.New("hello"), []int{CloseNormalClosure}, false},
|
||||
}
|
||||
|
||||
func TestCloseError(t *testing.T) {
|
||||
for _, tt := range closeErrorTests {
|
||||
ok := IsCloseError(tt.err, tt.codes...)
|
||||
if ok != tt.ok {
|
||||
t.Errorf("IsCloseError(%#v, %#v) returned %v, want %v", tt.err, tt.codes, ok, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unexpectedCloseErrorTests = []struct {
|
||||
err error
|
||||
codes []int
|
||||
ok bool
|
||||
}{
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNormalClosure}, false},
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNoStatusReceived}, true},
|
||||
{&CloseError{Code: CloseNormalClosure}, []int{CloseNoStatusReceived, CloseNormalClosure}, false},
|
||||
{errors.New("hello"), []int{CloseNormalClosure}, false},
|
||||
}
|
||||
|
||||
func TestUnexpectedCloseErrors(t *testing.T) {
|
||||
for _, tt := range unexpectedCloseErrorTests {
|
||||
ok := IsUnexpectedCloseError(tt.err, tt.codes...)
|
||||
if ok != tt.ok {
|
||||
t.Errorf("IsUnexpectedCloseError(%#v, %#v) returned %v, want %v", tt.err, tt.codes, ok, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type blockingWriter struct {
|
||||
c1, c2 chan struct{}
|
||||
}
|
||||
|
||||
func (w blockingWriter) Write(p []byte) (int, error) {
|
||||
// Allow main to continue
|
||||
close(w.c1)
|
||||
// Wait for panic in main
|
||||
<-w.c2
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TestConcurrentWritePanic(t *testing.T) {
|
||||
w := blockingWriter{make(chan struct{}), make(chan struct{})}
|
||||
c := newTestConn(nil, w, false)
|
||||
go func() {
|
||||
_ = c.WriteMessage(TextMessage, []byte{})
|
||||
}()
|
||||
|
||||
// wait for goroutine to block in write.
|
||||
<-w.c1
|
||||
|
||||
defer func() {
|
||||
close(w.c2)
|
||||
if v := recover(); v != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
_ = c.WriteMessage(TextMessage, []byte{})
|
||||
t.Fatal("should not get here")
|
||||
}
|
||||
|
||||
type failingReader struct{}
|
||||
|
||||
func (r failingReader) Read(p []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func TestFailedConnectionReadPanic(t *testing.T) {
|
||||
c := newTestConn(failingReader{}, nil, false)
|
||||
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < 20000; i++ {
|
||||
_, _, _ = c.ReadMessage()
|
||||
}
|
||||
t.Fatal("should not get here")
|
||||
}
|
||||
227
doc.go
Normal file
227
doc.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package websocket implements the WebSocket protocol defined in RFC 6455.
|
||||
//
|
||||
// Overview
|
||||
//
|
||||
// The Conn type represents a WebSocket connection. A server application calls
|
||||
// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn:
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// ReadBufferSize: 1024,
|
||||
// WriteBufferSize: 1024,
|
||||
// }
|
||||
//
|
||||
// func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// conn, err := upgrader.Upgrade(w, r, nil)
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return
|
||||
// }
|
||||
// ... Use conn to send and receive messages.
|
||||
// }
|
||||
//
|
||||
// Call the connection's WriteMessage and ReadMessage methods to send and
|
||||
// receive messages as a slice of bytes. This snippet of code shows how to echo
|
||||
// messages using these methods:
|
||||
//
|
||||
// for {
|
||||
// messageType, p, err := conn.ReadMessage()
|
||||
// if err != nil {
|
||||
// log.Println(err)
|
||||
// return
|
||||
// }
|
||||
// if err := conn.WriteMessage(messageType, p); err != nil {
|
||||
// log.Println(err)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// In above snippet of code, p is a []byte and messageType is an int with value
|
||||
// websocket.BinaryMessage or websocket.TextMessage.
|
||||
//
|
||||
// An application can also send and receive messages using the io.WriteCloser
|
||||
// and io.Reader interfaces. To send a message, call the connection NextWriter
|
||||
// method to get an io.WriteCloser, write the message to the writer and close
|
||||
// the writer when done. To receive a message, call the connection NextReader
|
||||
// method to get an io.Reader and read until io.EOF is returned. This snippet
|
||||
// shows how to echo messages using the NextWriter and NextReader methods:
|
||||
//
|
||||
// for {
|
||||
// messageType, r, err := conn.NextReader()
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// w, err := conn.NextWriter(messageType)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if _, err := io.Copy(w, r); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := w.Close(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Data Messages
|
||||
//
|
||||
// The WebSocket protocol distinguishes between text and binary data messages.
|
||||
// Text messages are interpreted as UTF-8 encoded text. The interpretation of
|
||||
// binary messages is left to the application.
|
||||
//
|
||||
// This package uses the TextMessage and BinaryMessage integer constants to
|
||||
// identify the two data message types. The ReadMessage and NextReader methods
|
||||
// return the type of the received message. The messageType argument to the
|
||||
// WriteMessage and NextWriter methods specifies the type of a sent message.
|
||||
//
|
||||
// It is the application's responsibility to ensure that text messages are
|
||||
// valid UTF-8 encoded text.
|
||||
//
|
||||
// Control Messages
|
||||
//
|
||||
// The WebSocket protocol defines three types of control messages: close, ping
|
||||
// and pong. Call the connection WriteControl, WriteMessage or NextWriter
|
||||
// methods to send a control message to the peer.
|
||||
//
|
||||
// Connections handle received close messages by calling the handler function
|
||||
// set with the SetCloseHandler method and by returning a *CloseError from the
|
||||
// NextReader, ReadMessage or the message Read method. The default close
|
||||
// handler sends a close message to the peer.
|
||||
//
|
||||
// Connections handle received ping messages by calling the handler function
|
||||
// set with the SetPingHandler method. The default ping handler sends a pong
|
||||
// message to the peer.
|
||||
//
|
||||
// Connections handle received pong messages by calling the handler function
|
||||
// set with the SetPongHandler method. The default pong handler does nothing.
|
||||
// If an application sends ping messages, then the application should set a
|
||||
// pong handler to receive the corresponding pong.
|
||||
//
|
||||
// The control message handler functions are called from the NextReader,
|
||||
// ReadMessage and message reader Read methods. The default close and ping
|
||||
// handlers can block these methods for a short time when the handler writes to
|
||||
// the connection.
|
||||
//
|
||||
// The application must read the connection to process close, ping and pong
|
||||
// messages sent from the peer. If the application is not otherwise interested
|
||||
// in messages from the peer, then the application should start a goroutine to
|
||||
// read and discard messages from the peer. A simple example is:
|
||||
//
|
||||
// func readLoop(c *websocket.Conn) {
|
||||
// for {
|
||||
// if _, _, err := c.NextReader(); err != nil {
|
||||
// c.Close()
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Concurrency
|
||||
//
|
||||
// Connections support one concurrent reader and one concurrent writer.
|
||||
//
|
||||
// Applications are responsible for ensuring that no more than one goroutine
|
||||
// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage,
|
||||
// WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and
|
||||
// that no more than one goroutine calls the read methods (NextReader,
|
||||
// SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler)
|
||||
// concurrently.
|
||||
//
|
||||
// The Close and WriteControl methods can be called concurrently with all other
|
||||
// methods.
|
||||
//
|
||||
// Origin Considerations
|
||||
//
|
||||
// Web browsers allow Javascript applications to open a WebSocket connection to
|
||||
// any host. It's up to the server to enforce an origin policy using the Origin
|
||||
// request header sent by the browser.
|
||||
//
|
||||
// The Upgrader calls the function specified in the CheckOrigin field to check
|
||||
// the origin. If the CheckOrigin function returns false, then the Upgrade
|
||||
// method fails the WebSocket handshake with HTTP status 403.
|
||||
//
|
||||
// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail
|
||||
// the handshake if the Origin request header is present and the Origin host is
|
||||
// not equal to the Host request header.
|
||||
//
|
||||
// The deprecated package-level Upgrade function does not perform origin
|
||||
// checking. The application is responsible for checking the Origin header
|
||||
// before calling the Upgrade function.
|
||||
//
|
||||
// Buffers
|
||||
//
|
||||
// Connections buffer network input and output to reduce the number
|
||||
// of system calls when reading or writing messages.
|
||||
//
|
||||
// Write buffers are also used for constructing WebSocket frames. See RFC 6455,
|
||||
// Section 5 for a discussion of message framing. A WebSocket frame header is
|
||||
// written to the network each time a write buffer is flushed to the network.
|
||||
// Decreasing the size of the write buffer can increase the amount of framing
|
||||
// overhead on the connection.
|
||||
//
|
||||
// The buffer sizes in bytes are specified by the ReadBufferSize and
|
||||
// WriteBufferSize fields in the Dialer and Upgrader. The Dialer uses a default
|
||||
// size of 4096 when a buffer size field is set to zero. The Upgrader reuses
|
||||
// buffers created by the HTTP server when a buffer size field is set to zero.
|
||||
// The HTTP server buffers have a size of 4096 at the time of this writing.
|
||||
//
|
||||
// The buffer sizes do not limit the size of a message that can be read or
|
||||
// written by a connection.
|
||||
//
|
||||
// Buffers are held for the lifetime of the connection by default. If the
|
||||
// Dialer or Upgrader WriteBufferPool field is set, then a connection holds the
|
||||
// write buffer only when writing a message.
|
||||
//
|
||||
// Applications should tune the buffer sizes to balance memory use and
|
||||
// performance. Increasing the buffer size uses more memory, but can reduce the
|
||||
// number of system calls to read or write the network. In the case of writing,
|
||||
// increasing the buffer size can reduce the number of frame headers written to
|
||||
// the network.
|
||||
//
|
||||
// Some guidelines for setting buffer parameters are:
|
||||
//
|
||||
// Limit the buffer sizes to the maximum expected message size. Buffers larger
|
||||
// than the largest message do not provide any benefit.
|
||||
//
|
||||
// Depending on the distribution of message sizes, setting the buffer size to
|
||||
// a value less than the maximum expected message size can greatly reduce memory
|
||||
// use with a small impact on performance. Here's an example: If 99% of the
|
||||
// messages are smaller than 256 bytes and the maximum message size is 512
|
||||
// bytes, then a buffer size of 256 bytes will result in 1.01 more system calls
|
||||
// than a buffer size of 512 bytes. The memory savings is 50%.
|
||||
//
|
||||
// A write buffer pool is useful when the application has a modest number
|
||||
// writes over a large number of connections. when buffers are pooled, a larger
|
||||
// buffer size has a reduced impact on total memory use and has the benefit of
|
||||
// reducing system calls and frame overhead.
|
||||
//
|
||||
// Compression EXPERIMENTAL
|
||||
//
|
||||
// Per message compression extensions (RFC 7692) are experimentally supported
|
||||
// by this package in a limited capacity. Setting the EnableCompression option
|
||||
// to true in Dialer or Upgrader will attempt to negotiate per message deflate
|
||||
// support.
|
||||
//
|
||||
// var upgrader = websocket.Upgrader{
|
||||
// EnableCompression: true,
|
||||
// }
|
||||
//
|
||||
// If compression was successfully negotiated with the connection's peer, any
|
||||
// message received in compressed form will be automatically decompressed.
|
||||
// All Read methods will return uncompressed bytes.
|
||||
//
|
||||
// Per message compression of messages written to a connection can be enabled
|
||||
// or disabled by calling the corresponding Conn method:
|
||||
//
|
||||
// conn.EnableWriteCompression(false)
|
||||
//
|
||||
// Currently this package does not support compression with "context takeover".
|
||||
// This means that messages must be compressed and decompressed in isolation,
|
||||
// without retaining sliding window or dictionary state across messages. For
|
||||
// more details refer to RFC 7692.
|
||||
//
|
||||
// Use of compression is experimental and may result in decreased performance.
|
||||
package websocket
|
||||
45
example_test.go
Normal file
45
example_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2015 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
c *websocket.Conn
|
||||
req *http.Request
|
||||
)
|
||||
|
||||
// The websocket.IsUnexpectedCloseError function is useful for identifying
|
||||
// application and protocol errors.
|
||||
//
|
||||
// This server application works with a client application running in the
|
||||
// browser. The client application does not explicitly close the websocket. The
|
||||
// only expected close message from the client has the code
|
||||
// websocket.CloseGoingAway. All other close messages are likely the
|
||||
// result of an application or protocol error and are logged to aid debugging.
|
||||
func ExampleIsUnexpectedCloseError() {
|
||||
for {
|
||||
messageType, p, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
||||
log.Printf("error: %v, user-agent: %v", err, req.Header.Get("User-Agent"))
|
||||
}
|
||||
return
|
||||
}
|
||||
processMessage(messageType, p)
|
||||
}
|
||||
}
|
||||
|
||||
func processMessage(mt int, p []byte) {}
|
||||
|
||||
// TestX prevents godoc from showing this entire file in the example. Remove
|
||||
// this function when a second example is added.
|
||||
func TestX(t *testing.T) {}
|
||||
18
examples/autobahn/README.md
Normal file
18
examples/autobahn/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Test Server
|
||||
|
||||
This package contains a server for the [Autobahn WebSockets Test Suite](https://github.com/crossbario/autobahn-testsuite).
|
||||
|
||||
To test the server, run
|
||||
|
||||
go run server.go
|
||||
|
||||
and start the client test driver
|
||||
|
||||
mkdir -p reports
|
||||
docker run -it --rm \
|
||||
-v ${PWD}/config:/config \
|
||||
-v ${PWD}/reports:/reports \
|
||||
crossbario/autobahn-testsuite \
|
||||
wstest -m fuzzingclient -s /config/fuzzingclient.json
|
||||
|
||||
When the client completes, it writes a report to reports/index.html.
|
||||
29
examples/autobahn/config/fuzzingclient.json
Normal file
29
examples/autobahn/config/fuzzingclient.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"cases": ["*"],
|
||||
"exclude-cases": [],
|
||||
"exclude-agent-cases": {},
|
||||
"outdir": "/reports",
|
||||
"options": {"failByDrop": false},
|
||||
"servers": [
|
||||
{
|
||||
"agent": "ReadAllWriteMessage",
|
||||
"url": "ws://host.docker.internal:9000/m"
|
||||
},
|
||||
{
|
||||
"agent": "ReadAllWritePreparedMessage",
|
||||
"url": "ws://host.docker.internal:9000/p"
|
||||
},
|
||||
{
|
||||
"agent": "CopyFull",
|
||||
"url": "ws://host.docker.internal:9000/f"
|
||||
},
|
||||
{
|
||||
"agent": "ReadAllWrite",
|
||||
"url": "ws://host.docker.internal:9000/r"
|
||||
},
|
||||
{
|
||||
"agent": "CopyWriterOnly",
|
||||
"url": "ws://host.docker.internal:9000/c"
|
||||
}
|
||||
]
|
||||
}
|
||||
265
examples/autobahn/server.go
Normal file
265
examples/autobahn/server.go
Normal file
@@ -0,0 +1,265 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Command server is a test server for the Autobahn WebSockets Test Suite.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
EnableCompression: true,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// echoCopy echoes messages from the client using io.Copy.
|
||||
func echoCopy(w http.ResponseWriter, r *http.Request, writerOnly bool) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("Upgrade:", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
for {
|
||||
mt, r, err := conn.NextReader()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Println("NextReader:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if mt == websocket.TextMessage {
|
||||
r = &validator{r: r}
|
||||
}
|
||||
w, err := conn.NextWriter(mt)
|
||||
if err != nil {
|
||||
log.Println("NextWriter:", err)
|
||||
return
|
||||
}
|
||||
if mt == websocket.TextMessage {
|
||||
r = &validator{r: r}
|
||||
}
|
||||
if writerOnly {
|
||||
_, err = io.Copy(struct{ io.Writer }{w}, r)
|
||||
} else {
|
||||
_, err = io.Copy(w, r)
|
||||
}
|
||||
if err != nil {
|
||||
if err == errInvalidUTF8 {
|
||||
conn.WriteControl(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseInvalidFramePayloadData, ""),
|
||||
time.Time{})
|
||||
}
|
||||
log.Println("Copy:", err)
|
||||
return
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
log.Println("Close:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func echoCopyWriterOnly(w http.ResponseWriter, r *http.Request) {
|
||||
echoCopy(w, r, true)
|
||||
}
|
||||
|
||||
func echoCopyFull(w http.ResponseWriter, r *http.Request) {
|
||||
echoCopy(w, r, false)
|
||||
}
|
||||
|
||||
// echoReadAll echoes messages from the client by reading the entire message
|
||||
// with io.ReadAll.
|
||||
func echoReadAll(w http.ResponseWriter, r *http.Request, writeMessage, writePrepared bool) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("Upgrade:", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
for {
|
||||
mt, b, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Println("NextReader:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if mt == websocket.TextMessage {
|
||||
if !utf8.Valid(b) {
|
||||
conn.WriteControl(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseInvalidFramePayloadData, ""),
|
||||
time.Time{})
|
||||
log.Println("ReadAll: invalid utf8")
|
||||
}
|
||||
}
|
||||
if writeMessage {
|
||||
if !writePrepared {
|
||||
err = conn.WriteMessage(mt, b)
|
||||
if err != nil {
|
||||
log.Println("WriteMessage:", err)
|
||||
}
|
||||
} else {
|
||||
pm, err := websocket.NewPreparedMessage(mt, b)
|
||||
if err != nil {
|
||||
log.Println("NewPreparedMessage:", err)
|
||||
return
|
||||
}
|
||||
err = conn.WritePreparedMessage(pm)
|
||||
if err != nil {
|
||||
log.Println("WritePreparedMessage:", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
w, err := conn.NextWriter(mt)
|
||||
if err != nil {
|
||||
log.Println("NextWriter:", err)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(b); err != nil {
|
||||
log.Println("Writer:", err)
|
||||
return
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
log.Println("Close:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func echoReadAllWriter(w http.ResponseWriter, r *http.Request) {
|
||||
echoReadAll(w, r, false, false)
|
||||
}
|
||||
|
||||
func echoReadAllWriteMessage(w http.ResponseWriter, r *http.Request) {
|
||||
echoReadAll(w, r, true, false)
|
||||
}
|
||||
|
||||
func echoReadAllWritePreparedMessage(w http.ResponseWriter, r *http.Request) {
|
||||
echoReadAll(w, r, true, true)
|
||||
}
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Not found.", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
io.WriteString(w, "<html><body>Echo Server</body></html>")
|
||||
}
|
||||
|
||||
var addr = flag.String("addr", ":9000", "http service address")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/c", echoCopyWriterOnly)
|
||||
http.HandleFunc("/f", echoCopyFull)
|
||||
http.HandleFunc("/r", echoReadAllWriter)
|
||||
http.HandleFunc("/m", echoReadAllWriteMessage)
|
||||
http.HandleFunc("/p", echoReadAllWritePreparedMessage)
|
||||
err := http.ListenAndServe(*addr, nil)
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
type validator struct {
|
||||
state int
|
||||
x rune
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
var errInvalidUTF8 = errors.New("invalid utf8")
|
||||
|
||||
func (r *validator) Read(p []byte) (int, error) {
|
||||
n, err := r.r.Read(p)
|
||||
state := r.state
|
||||
x := r.x
|
||||
for _, b := range p[:n] {
|
||||
state, x = decode(state, x, b)
|
||||
if state == utf8Reject {
|
||||
break
|
||||
}
|
||||
}
|
||||
r.state = state
|
||||
r.x = x
|
||||
if state == utf8Reject || (err == io.EOF && state != utf8Accept) {
|
||||
return n, errInvalidUTF8
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// UTF-8 decoder from http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
||||
//
|
||||
// Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
var utf8d = [...]byte{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1f
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3f
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5f
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7f
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9f
|
||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // a0..bf
|
||||
8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // c0..df
|
||||
0xa, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // e0..ef
|
||||
0xb, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // f0..ff
|
||||
0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2
|
||||
1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4
|
||||
1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6
|
||||
1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // s7..s8
|
||||
}
|
||||
|
||||
const (
|
||||
utf8Accept = 0
|
||||
utf8Reject = 1
|
||||
)
|
||||
|
||||
func decode(state int, x rune, b byte) (int, rune) {
|
||||
t := utf8d[b]
|
||||
if state != utf8Accept {
|
||||
x = rune(b&0x3f) | (x << 6)
|
||||
} else {
|
||||
x = rune((0xff >> t) & b)
|
||||
}
|
||||
state = int(utf8d[256+state*16+int(t)])
|
||||
return state, x
|
||||
}
|
||||
102
examples/chat/README.md
Normal file
102
examples/chat/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Chat Example
|
||||
|
||||
This application shows how to use the
|
||||
[websocket](https://github.com/gorilla/websocket) package to implement a simple
|
||||
web chat application.
|
||||
|
||||
## Running the example
|
||||
|
||||
The example requires a working Go development environment. The [Getting
|
||||
Started](http://golang.org/doc/install) page describes how to install the
|
||||
development environment.
|
||||
|
||||
Once you have Go up and running, you can download, build and run the example
|
||||
using the following commands.
|
||||
|
||||
$ go get github.com/gorilla/websocket
|
||||
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/chat`
|
||||
$ go run *.go
|
||||
|
||||
To use the chat example, open http://localhost:8080/ in your browser.
|
||||
|
||||
## Server
|
||||
|
||||
The server application defines two types, `Client` and `Hub`. The server
|
||||
creates an instance of the `Client` type for each websocket connection. A
|
||||
`Client` acts as an intermediary between the websocket connection and a single
|
||||
instance of the `Hub` type. The `Hub` maintains a set of registered clients and
|
||||
broadcasts messages to the clients.
|
||||
|
||||
The application runs one goroutine for the `Hub` and two goroutines for each
|
||||
`Client`. The goroutines communicate with each other using channels. The `Hub`
|
||||
has channels for registering clients, unregistering clients and broadcasting
|
||||
messages. A `Client` has a buffered channel of outbound messages. One of the
|
||||
client's goroutines reads messages from this channel and writes the messages to
|
||||
the websocket. The other client goroutine reads messages from the websocket and
|
||||
sends them to the hub.
|
||||
|
||||
### Hub
|
||||
|
||||
The code for the `Hub` type is in
|
||||
[hub.go](https://github.com/gorilla/websocket/blob/main/examples/chat/hub.go).
|
||||
The application's `main` function starts the hub's `run` method as a goroutine.
|
||||
Clients send requests to the hub using the `register`, `unregister` and
|
||||
`broadcast` channels.
|
||||
|
||||
The hub registers clients by adding the client pointer as a key in the
|
||||
`clients` map. The map value is always true.
|
||||
|
||||
The unregister code is a little more complicated. In addition to deleting the
|
||||
client pointer from the `clients` map, the hub closes the clients's `send`
|
||||
channel to signal the client that no more messages will be sent to the client.
|
||||
|
||||
The hub handles messages by looping over the registered clients and sending the
|
||||
message to the client's `send` channel. If the client's `send` buffer is full,
|
||||
then the hub assumes that the client is dead or stuck. In this case, the hub
|
||||
unregisters the client and closes the websocket.
|
||||
|
||||
### Client
|
||||
|
||||
The code for the `Client` type is in [client.go](https://github.com/gorilla/websocket/blob/main/examples/chat/client.go).
|
||||
|
||||
The `serveWs` function is registered by the application's `main` function as
|
||||
an HTTP handler. The handler upgrades the HTTP connection to the WebSocket
|
||||
protocol, creates a client, registers the client with the hub and schedules the
|
||||
client to be unregistered using a defer statement.
|
||||
|
||||
Next, the HTTP handler starts the client's `writePump` method as a goroutine.
|
||||
This method transfers messages from the client's send channel to the websocket
|
||||
connection. The writer method exits when the channel is closed by the hub or
|
||||
there's an error writing to the websocket connection.
|
||||
|
||||
Finally, the HTTP handler calls the client's `readPump` method. This method
|
||||
transfers inbound messages from the websocket to the hub.
|
||||
|
||||
WebSocket connections [support one concurrent reader and one concurrent
|
||||
writer](https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency). The
|
||||
application ensures that these concurrency requirements are met by executing
|
||||
all reads from the `readPump` goroutine and all writes from the `writePump`
|
||||
goroutine.
|
||||
|
||||
To improve efficiency under high load, the `writePump` function coalesces
|
||||
pending chat messages in the `send` channel to a single WebSocket message. This
|
||||
reduces the number of system calls and the amount of data sent over the
|
||||
network.
|
||||
|
||||
## Frontend
|
||||
|
||||
The frontend code is in [home.html](https://github.com/gorilla/websocket/blob/main/examples/chat/home.html).
|
||||
|
||||
On document load, the script checks for websocket functionality in the browser.
|
||||
If websocket functionality is available, then the script opens a connection to
|
||||
the server and registers a callback to handle messages from the server. The
|
||||
callback appends the message to the chat log using the appendLog function.
|
||||
|
||||
To allow the user to manually scroll through the chat log without interruption
|
||||
from new messages, the `appendLog` function checks the scroll position before
|
||||
adding new content. If the chat log is scrolled to the bottom, then the
|
||||
function scrolls new content into view after adding the content. Otherwise, the
|
||||
scroll position is not changed.
|
||||
|
||||
The form handler writes the user input to the websocket and clears the input
|
||||
field.
|
||||
137
examples/chat/client.go
Normal file
137
examples/chat/client.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the peer.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Maximum message size allowed from peer.
|
||||
maxMessageSize = 512
|
||||
)
|
||||
|
||||
var (
|
||||
newline = []byte{'\n'}
|
||||
space = []byte{' '}
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
// Client is a middleman between the websocket connection and the hub.
|
||||
type Client struct {
|
||||
hub *Hub
|
||||
|
||||
// The websocket connection.
|
||||
conn *websocket.Conn
|
||||
|
||||
// Buffered channel of outbound messages.
|
||||
send chan []byte
|
||||
}
|
||||
|
||||
// readPump pumps messages from the websocket connection to the hub.
|
||||
//
|
||||
// The application runs readPump in a per-connection goroutine. The application
|
||||
// ensures that there is at most one reader on a connection by executing all
|
||||
// reads from this goroutine.
|
||||
func (c *Client) readPump() {
|
||||
defer func() {
|
||||
c.hub.unregister <- c
|
||||
c.conn.Close()
|
||||
}()
|
||||
c.conn.SetReadLimit(maxMessageSize)
|
||||
c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||
for {
|
||||
_, message, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
log.Printf("error: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
|
||||
c.hub.broadcast <- message
|
||||
}
|
||||
}
|
||||
|
||||
// writePump pumps messages from the hub to the websocket connection.
|
||||
//
|
||||
// A goroutine running writePump is started for each connection. The
|
||||
// application ensures that there is at most one writer to a connection by
|
||||
// executing all writes from this goroutine.
|
||||
func (c *Client) writePump() {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
c.conn.Close()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-c.send:
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if !ok {
|
||||
// The hub closed the channel.
|
||||
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
|
||||
w, err := c.conn.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
w.Write(message)
|
||||
|
||||
// Add queued chat messages to the current websocket message.
|
||||
n := len(c.send)
|
||||
for i := 0; i < n; i++ {
|
||||
w.Write(newline)
|
||||
w.Write(<-c.send)
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serveWs handles websocket requests from the peer.
|
||||
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
|
||||
client.hub.register <- client
|
||||
|
||||
// Allow collection of memory referenced by the caller by doing all work in
|
||||
// new goroutines.
|
||||
go client.writePump()
|
||||
go client.readPump()
|
||||
}
|
||||
98
examples/chat/home.html
Normal file
98
examples/chat/home.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Chat Example</title>
|
||||
<script type="text/javascript">
|
||||
window.onload = function () {
|
||||
var conn;
|
||||
var msg = document.getElementById("msg");
|
||||
var log = document.getElementById("log");
|
||||
|
||||
function appendLog(item) {
|
||||
var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1;
|
||||
log.appendChild(item);
|
||||
if (doScroll) {
|
||||
log.scrollTop = log.scrollHeight - log.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("form").onsubmit = function () {
|
||||
if (!conn) {
|
||||
return false;
|
||||
}
|
||||
if (!msg.value) {
|
||||
return false;
|
||||
}
|
||||
conn.send(msg.value);
|
||||
msg.value = "";
|
||||
return false;
|
||||
};
|
||||
|
||||
if (window["WebSocket"]) {
|
||||
conn = new WebSocket("ws://" + document.location.host + "/ws");
|
||||
conn.onclose = function (evt) {
|
||||
var item = document.createElement("div");
|
||||
item.innerHTML = "<b>Connection closed.</b>";
|
||||
appendLog(item);
|
||||
};
|
||||
conn.onmessage = function (evt) {
|
||||
var messages = evt.data.split('\n');
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
var item = document.createElement("div");
|
||||
item.innerText = messages[i];
|
||||
appendLog(item);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
var item = document.createElement("div");
|
||||
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
|
||||
appendLog(item);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style type="text/css">
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
#log {
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding: 0.5em 0.5em 0.5em 0.5em;
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
right: 0.5em;
|
||||
bottom: 3em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#form {
|
||||
padding: 0 0.5em 0 0.5em;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="log"></div>
|
||||
<form id="form">
|
||||
<input type="submit" value="Send" />
|
||||
<input type="text" id="msg" size="64" autofocus />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
53
examples/chat/hub.go
Normal file
53
examples/chat/hub.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
// Hub maintains the set of active clients and broadcasts messages to the
|
||||
// clients.
|
||||
type Hub struct {
|
||||
// Registered clients.
|
||||
clients map[*Client]bool
|
||||
|
||||
// Inbound messages from the clients.
|
||||
broadcast chan []byte
|
||||
|
||||
// Register requests from the clients.
|
||||
register chan *Client
|
||||
|
||||
// Unregister requests from clients.
|
||||
unregister chan *Client
|
||||
}
|
||||
|
||||
func newHub() *Hub {
|
||||
return &Hub{
|
||||
broadcast: make(chan []byte),
|
||||
register: make(chan *Client),
|
||||
unregister: make(chan *Client),
|
||||
clients: make(map[*Client]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) run() {
|
||||
for {
|
||||
select {
|
||||
case client := <-h.register:
|
||||
h.clients[client] = true
|
||||
case client := <-h.unregister:
|
||||
if _, ok := h.clients[client]; ok {
|
||||
delete(h.clients, client)
|
||||
close(client.send)
|
||||
}
|
||||
case message := <-h.broadcast:
|
||||
for client := range h.clients {
|
||||
select {
|
||||
case client.send <- message:
|
||||
default:
|
||||
close(client.send)
|
||||
delete(h.clients, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
examples/chat/main.go
Normal file
40
examples/chat/main.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var addr = flag.String("addr", ":8080", "http service address")
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println(r.URL)
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, "home.html")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
hub := newHub()
|
||||
go hub.run()
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
serveWs(hub, w, r)
|
||||
})
|
||||
err := http.ListenAndServe(*addr, nil)
|
||||
if err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
19
examples/command/README.md
Normal file
19
examples/command/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Command example
|
||||
|
||||
This example connects a websocket connection to stdin and stdout of a command.
|
||||
Received messages are written to stdin followed by a `\n`. Each line read from
|
||||
standard out is sent as a message to the client.
|
||||
|
||||
$ go get github.com/gorilla/websocket
|
||||
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/command`
|
||||
$ go run main.go <command and arguments to run>
|
||||
# Open http://localhost:8080/ .
|
||||
|
||||
Try the following commands.
|
||||
|
||||
# Echo sent messages to the output area.
|
||||
$ go run main.go cat
|
||||
|
||||
# Run a shell.Try sending "ls" and "cat main.go".
|
||||
$ go run main.go sh
|
||||
|
||||
102
examples/command/home.html
Normal file
102
examples/command/home.html
Normal file
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Command Example</title>
|
||||
<script type="text/javascript">
|
||||
window.onload = function () {
|
||||
var conn;
|
||||
var msg = document.getElementById("msg");
|
||||
var log = document.getElementById("log");
|
||||
|
||||
function appendLog(item) {
|
||||
var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1;
|
||||
log.appendChild(item);
|
||||
if (doScroll) {
|
||||
log.scrollTop = log.scrollHeight - log.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("form").onsubmit = function () {
|
||||
if (!conn) {
|
||||
return false;
|
||||
}
|
||||
if (!msg.value) {
|
||||
return false;
|
||||
}
|
||||
conn.send(msg.value);
|
||||
msg.value = "";
|
||||
return false;
|
||||
};
|
||||
|
||||
if (window["WebSocket"]) {
|
||||
conn = new WebSocket("ws://" + document.location.host + "/ws");
|
||||
conn.onclose = function (evt) {
|
||||
var item = document.createElement("div");
|
||||
item.innerHTML = "<b>Connection closed.</b>";
|
||||
appendLog(item);
|
||||
};
|
||||
conn.onmessage = function (evt) {
|
||||
var messages = evt.data.split('\n');
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
var item = document.createElement("div");
|
||||
item.innerText = messages[i];
|
||||
appendLog(item);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
var item = document.createElement("div");
|
||||
item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
|
||||
appendLog(item);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style type="text/css">
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: gray;
|
||||
}
|
||||
|
||||
#log {
|
||||
background: white;
|
||||
margin: 0;
|
||||
padding: 0.5em 0.5em 0.5em 0.5em;
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
right: 0.5em;
|
||||
bottom: 3em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#log pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#form {
|
||||
padding: 0 0.5em 0 0.5em;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="log"></div>
|
||||
<form id="form">
|
||||
<input type="submit" value="Send" />
|
||||
<input type="text" id="msg" size="64"/>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
193
examples/command/main.go
Normal file
193
examples/command/main.go
Normal file
@@ -0,0 +1,193 @@
|
||||
// Copyright 2015 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", "127.0.0.1:8080", "http service address")
|
||||
cmdPath string
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Maximum message size allowed from peer.
|
||||
maxMessageSize = 8192
|
||||
|
||||
// Time allowed to read the next pong message from the peer.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Time to wait before force close on connection.
|
||||
closeGracePeriod = 10 * time.Second
|
||||
)
|
||||
|
||||
func pumpStdin(ws *websocket.Conn, w io.Writer) {
|
||||
defer ws.Close()
|
||||
ws.SetReadLimit(maxMessageSize)
|
||||
ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||
for {
|
||||
_, message, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
message = append(message, '\n')
|
||||
if _, err := w.Write(message); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pumpStdout(ws *websocket.Conn, r io.Reader, done chan struct{}) {
|
||||
defer func() {
|
||||
}()
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := ws.WriteMessage(websocket.TextMessage, s.Bytes()); err != nil {
|
||||
ws.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
if s.Err() != nil {
|
||||
log.Println("scan:", s.Err())
|
||||
}
|
||||
close(done)
|
||||
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
time.Sleep(closeGracePeriod)
|
||||
ws.Close()
|
||||
}
|
||||
|
||||
func ping(ws *websocket.Conn, done chan struct{}) {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil {
|
||||
log.Println("ping:", err)
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func internalError(ws *websocket.Conn, msg string, err error) {
|
||||
log.Println(msg, err)
|
||||
ws.WriteMessage(websocket.TextMessage, []byte("Internal server error."))
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{}
|
||||
|
||||
func serveWs(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("upgrade:", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer ws.Close()
|
||||
|
||||
outr, outw, err := os.Pipe()
|
||||
if err != nil {
|
||||
internalError(ws, "stdout:", err)
|
||||
return
|
||||
}
|
||||
defer outr.Close()
|
||||
defer outw.Close()
|
||||
|
||||
inr, inw, err := os.Pipe()
|
||||
if err != nil {
|
||||
internalError(ws, "stdin:", err)
|
||||
return
|
||||
}
|
||||
defer inr.Close()
|
||||
defer inw.Close()
|
||||
|
||||
proc, err := os.StartProcess(cmdPath, flag.Args(), &os.ProcAttr{
|
||||
Files: []*os.File{inr, outw, outw},
|
||||
})
|
||||
if err != nil {
|
||||
internalError(ws, "start:", err)
|
||||
return
|
||||
}
|
||||
|
||||
inr.Close()
|
||||
outw.Close()
|
||||
|
||||
stdoutDone := make(chan struct{})
|
||||
go pumpStdout(ws, outr, stdoutDone)
|
||||
go ping(ws, stdoutDone)
|
||||
|
||||
pumpStdin(ws, inw)
|
||||
|
||||
// Some commands will exit when stdin is closed.
|
||||
inw.Close()
|
||||
|
||||
// Other commands need a bonk on the head.
|
||||
if err := proc.Signal(os.Interrupt); err != nil {
|
||||
log.Println("inter:", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-stdoutDone:
|
||||
case <-time.After(time.Second):
|
||||
// A bigger bonk on the head.
|
||||
if err := proc.Signal(os.Kill); err != nil {
|
||||
log.Println("term:", err)
|
||||
}
|
||||
<-stdoutDone
|
||||
}
|
||||
|
||||
if _, err := proc.Wait(); err != nil {
|
||||
log.Println("wait:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, "home.html")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if len(flag.Args()) < 1 {
|
||||
log.Fatal("must specify at least one argument")
|
||||
}
|
||||
var err error
|
||||
cmdPath, err = exec.LookPath(flag.Args()[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/ws", serveWs)
|
||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||
}
|
||||
17
examples/echo/README.md
Normal file
17
examples/echo/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Client and server example
|
||||
|
||||
This example shows a simple client and server.
|
||||
|
||||
The server echoes messages sent to it. The client sends a message every second
|
||||
and prints all messages received.
|
||||
|
||||
To run the example, start the server:
|
||||
|
||||
$ go run server.go
|
||||
|
||||
Next, start the client:
|
||||
|
||||
$ go run client.go
|
||||
|
||||
The server includes a simple web client. To use the client, open
|
||||
http://127.0.0.1:8080 in the browser and follow the instructions on the page.
|
||||
83
examples/echo/client.go
Normal file
83
examples/echo/client.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2015 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var addr = flag.String("addr", "localhost:8080", "http service address")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log.SetFlags(0)
|
||||
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
|
||||
u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo"}
|
||||
log.Printf("connecting to %s", u.String())
|
||||
|
||||
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
||||
if err != nil {
|
||||
log.Fatal("dial:", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
for {
|
||||
_, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("read:", err)
|
||||
return
|
||||
}
|
||||
log.Printf("recv: %s", message)
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case t := <-ticker.C:
|
||||
err := c.WriteMessage(websocket.TextMessage, []byte(t.String()))
|
||||
if err != nil {
|
||||
log.Println("write:", err)
|
||||
return
|
||||
}
|
||||
case <-interrupt:
|
||||
log.Println("interrupt")
|
||||
|
||||
// Cleanly close the connection by sending a close message and then
|
||||
// waiting (with timeout) for the server to close the connection.
|
||||
err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
if err != nil {
|
||||
log.Println("write close:", err)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
135
examples/echo/server.go
Normal file
135
examples/echo/server.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2015 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var addr = flag.String("addr", "localhost:8080", "http service address")
|
||||
|
||||
var upgrader = websocket.Upgrader{} // use default options
|
||||
|
||||
func echo(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Print("upgrade:", err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
for {
|
||||
mt, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("read:", err)
|
||||
break
|
||||
}
|
||||
log.Printf("recv: %s", message)
|
||||
err = c.WriteMessage(mt, message)
|
||||
if err != nil {
|
||||
log.Println("write:", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func home(w http.ResponseWriter, r *http.Request) {
|
||||
homeTemplate.Execute(w, "ws://"+r.Host+"/echo")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log.SetFlags(0)
|
||||
http.HandleFunc("/echo", echo)
|
||||
http.HandleFunc("/", home)
|
||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||
}
|
||||
|
||||
var homeTemplate = template.Must(template.New("").Parse(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
window.addEventListener("load", function(evt) {
|
||||
|
||||
var output = document.getElementById("output");
|
||||
var input = document.getElementById("input");
|
||||
var ws;
|
||||
|
||||
var print = function(message) {
|
||||
var d = document.createElement("div");
|
||||
d.textContent = message;
|
||||
output.appendChild(d);
|
||||
output.scroll(0, output.scrollHeight);
|
||||
};
|
||||
|
||||
document.getElementById("open").onclick = function(evt) {
|
||||
if (ws) {
|
||||
return false;
|
||||
}
|
||||
ws = new WebSocket("{{.}}");
|
||||
ws.onopen = function(evt) {
|
||||
print("OPEN");
|
||||
}
|
||||
ws.onclose = function(evt) {
|
||||
print("CLOSE");
|
||||
ws = null;
|
||||
}
|
||||
ws.onmessage = function(evt) {
|
||||
print("RESPONSE: " + evt.data);
|
||||
}
|
||||
ws.onerror = function(evt) {
|
||||
print("ERROR: " + evt.data);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById("send").onclick = function(evt) {
|
||||
if (!ws) {
|
||||
return false;
|
||||
}
|
||||
print("SEND: " + input.value);
|
||||
ws.send(input.value);
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById("close").onclick = function(evt) {
|
||||
if (!ws) {
|
||||
return false;
|
||||
}
|
||||
ws.close();
|
||||
return false;
|
||||
};
|
||||
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<table>
|
||||
<tr><td valign="top" width="50%">
|
||||
<p>Click "Open" to create a connection to the server,
|
||||
"Send" to send a message to the server and "Close" to close the connection.
|
||||
You can change the message and send multiple times.
|
||||
<p>
|
||||
<form>
|
||||
<button id="open">Open</button>
|
||||
<button id="close">Close</button>
|
||||
<p><input id="input" type="text" value="Hello world!">
|
||||
<button id="send">Send</button>
|
||||
</form>
|
||||
</td><td valign="top" width="50%">
|
||||
<div id="output" style="max-height: 70vh;overflow-y: scroll;"></div>
|
||||
</td></tr></table>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
9
examples/filewatch/README.md
Normal file
9
examples/filewatch/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# File Watch example.
|
||||
|
||||
This example sends a file to the browser client for display whenever the file is modified.
|
||||
|
||||
$ go get github.com/gorilla/websocket
|
||||
$ cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/filewatch`
|
||||
$ go run main.go <name of file to watch>
|
||||
# Open http://localhost:8080/ .
|
||||
# Modify the file to see it update in the browser.
|
||||
192
examples/filewatch/main.go
Normal file
192
examples/filewatch/main.go
Normal file
@@ -0,0 +1,192 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write the file to the client.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the client.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to client with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Poll file for changes with this period.
|
||||
filePeriod = 10 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":8080", "http service address")
|
||||
homeTempl = template.Must(template.New("").Parse(homeHTML))
|
||||
filename string
|
||||
upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
)
|
||||
|
||||
func readFileIfModified(lastMod time.Time) ([]byte, time.Time, error) {
|
||||
fi, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return nil, lastMod, err
|
||||
}
|
||||
if !fi.ModTime().After(lastMod) {
|
||||
return nil, lastMod, nil
|
||||
}
|
||||
p, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fi.ModTime(), err
|
||||
}
|
||||
return p, fi.ModTime(), nil
|
||||
}
|
||||
|
||||
func reader(ws *websocket.Conn) {
|
||||
defer ws.Close()
|
||||
ws.SetReadLimit(512)
|
||||
ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||
for {
|
||||
_, _, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writer(ws *websocket.Conn, lastMod time.Time) {
|
||||
lastError := ""
|
||||
pingTicker := time.NewTicker(pingPeriod)
|
||||
fileTicker := time.NewTicker(filePeriod)
|
||||
defer func() {
|
||||
pingTicker.Stop()
|
||||
fileTicker.Stop()
|
||||
ws.Close()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-fileTicker.C:
|
||||
var p []byte
|
||||
var err error
|
||||
|
||||
p, lastMod, err = readFileIfModified(lastMod)
|
||||
|
||||
if err != nil {
|
||||
if s := err.Error(); s != lastError {
|
||||
lastError = s
|
||||
p = []byte(lastError)
|
||||
}
|
||||
} else {
|
||||
lastError = ""
|
||||
}
|
||||
|
||||
if p != nil {
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-pingTicker.C:
|
||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func serveWs(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
if _, ok := err.(websocket.HandshakeError); !ok {
|
||||
log.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var lastMod time.Time
|
||||
if n, err := strconv.ParseInt(r.FormValue("lastMod"), 16, 64); err == nil {
|
||||
lastMod = time.Unix(0, n)
|
||||
}
|
||||
|
||||
go writer(ws, lastMod)
|
||||
reader(ws)
|
||||
}
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
p, lastMod, err := readFileIfModified(time.Time{})
|
||||
if err != nil {
|
||||
p = []byte(err.Error())
|
||||
lastMod = time.Unix(0, 0)
|
||||
}
|
||||
var v = struct {
|
||||
Host string
|
||||
Data string
|
||||
LastMod string
|
||||
}{
|
||||
r.Host,
|
||||
string(p),
|
||||
strconv.FormatInt(lastMod.UnixNano(), 16),
|
||||
}
|
||||
homeTempl.Execute(w, &v)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if flag.NArg() != 1 {
|
||||
log.Fatal("filename not specified")
|
||||
}
|
||||
filename = flag.Args()[0]
|
||||
http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/ws", serveWs)
|
||||
if err := http.ListenAndServe(*addr, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
const homeHTML = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>WebSocket Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="fileData">{{.Data}}</pre>
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var data = document.getElementById("fileData");
|
||||
var conn = new WebSocket("ws://{{.Host}}/ws?lastMod={{.LastMod}}");
|
||||
conn.onclose = function(evt) {
|
||||
data.textContent = 'Connection closed';
|
||||
}
|
||||
conn.onmessage = function(evt) {
|
||||
console.log('file updated');
|
||||
data.textContent = evt.data;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
9
go.mod
Normal file
9
go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module github.com/gorilla/websocket
|
||||
|
||||
go 1.20
|
||||
|
||||
retract (
|
||||
v1.5.2 // tag accidentally overwritten
|
||||
)
|
||||
|
||||
require golang.org/x/net v0.26.0
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
42
join.go
Normal file
42
join.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2019 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JoinMessages concatenates received messages to create a single io.Reader.
|
||||
// The string term is appended to each message. The returned reader does not
|
||||
// support concurrent calls to the Read method.
|
||||
func JoinMessages(c *Conn, term string) io.Reader {
|
||||
return &joinReader{c: c, term: term}
|
||||
}
|
||||
|
||||
type joinReader struct {
|
||||
c *Conn
|
||||
term string
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (r *joinReader) Read(p []byte) (int, error) {
|
||||
if r.r == nil {
|
||||
var err error
|
||||
_, r.r, err = r.c.NextReader()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if r.term != "" {
|
||||
r.r = io.MultiReader(r.r, strings.NewReader(r.term))
|
||||
}
|
||||
}
|
||||
n, err := r.r.Read(p)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
r.r = nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
36
join_test.go
Normal file
36
join_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2019 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJoinMessages(t *testing.T) {
|
||||
messages := []string{"a", "bc", "def", "ghij", "klmno", "0", "12", "345", "6789"}
|
||||
for _, readChunk := range []int{1, 2, 3, 4, 5, 6, 7} {
|
||||
for _, term := range []string{"", ","} {
|
||||
var connBuf bytes.Buffer
|
||||
wc := newTestConn(nil, &connBuf, true)
|
||||
rc := newTestConn(&connBuf, nil, false)
|
||||
for _, m := range messages {
|
||||
_ = wc.WriteMessage(BinaryMessage, []byte(m))
|
||||
}
|
||||
|
||||
var result bytes.Buffer
|
||||
_, err := io.CopyBuffer(&result, JoinMessages(rc, term), make([]byte, readChunk))
|
||||
if IsUnexpectedCloseError(err, CloseAbnormalClosure) {
|
||||
t.Errorf("readChunk=%d, term=%q: unexpected error %v", readChunk, term, err)
|
||||
}
|
||||
want := strings.Join(messages, term) + term
|
||||
if result.String() != want {
|
||||
t.Errorf("readChunk=%d, term=%q, got %q, want %q", readChunk, term, result.String(), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
json.go
Normal file
60
json.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
// WriteJSON writes the JSON encoding of v as a message.
|
||||
//
|
||||
// Deprecated: Use c.WriteJSON instead.
|
||||
func WriteJSON(c *Conn, v interface{}) error {
|
||||
return c.WriteJSON(v)
|
||||
}
|
||||
|
||||
// WriteJSON writes the JSON encoding of v as a message.
|
||||
//
|
||||
// See the documentation for encoding/json Marshal for details about the
|
||||
// conversion of Go values to JSON.
|
||||
func (c *Conn) WriteJSON(v interface{}) error {
|
||||
w, err := c.NextWriter(TextMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err1 := json.NewEncoder(w).Encode(v)
|
||||
err2 := w.Close()
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
// ReadJSON reads the next JSON-encoded message from the connection and stores
|
||||
// it in the value pointed to by v.
|
||||
//
|
||||
// Deprecated: Use c.ReadJSON instead.
|
||||
func ReadJSON(c *Conn, v interface{}) error {
|
||||
return c.ReadJSON(v)
|
||||
}
|
||||
|
||||
// ReadJSON reads the next JSON-encoded message from the connection and stores
|
||||
// it in the value pointed to by v.
|
||||
//
|
||||
// See the documentation for the encoding/json Unmarshal function for details
|
||||
// about the conversion of JSON to a Go value.
|
||||
func (c *Conn) ReadJSON(v interface{}) error {
|
||||
_, r, err := c.NextReader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.NewDecoder(r).Decode(v)
|
||||
if err == io.EOF {
|
||||
// One value is expected in the message.
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return err
|
||||
}
|
||||
116
json_test.go
Normal file
116
json_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
wc := newTestConn(nil, &buf, true)
|
||||
rc := newTestConn(&buf, nil, false)
|
||||
|
||||
var actual, expect struct {
|
||||
A int
|
||||
B string
|
||||
}
|
||||
expect.A = 1
|
||||
expect.B = "hello"
|
||||
|
||||
if err := wc.WriteJSON(&expect); err != nil {
|
||||
t.Fatal("write", err)
|
||||
}
|
||||
|
||||
if err := rc.ReadJSON(&actual); err != nil {
|
||||
t.Fatal("read", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(&actual, &expect) {
|
||||
t.Fatal("equal", actual, expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPartialJSONRead(t *testing.T) {
|
||||
var buf0, buf1 bytes.Buffer
|
||||
wc := newTestConn(nil, &buf0, true)
|
||||
rc := newTestConn(&buf0, &buf1, false)
|
||||
|
||||
var v struct {
|
||||
A int
|
||||
B string
|
||||
}
|
||||
v.A = 1
|
||||
v.B = "hello"
|
||||
|
||||
messageCount := 0
|
||||
|
||||
// Partial JSON values.
|
||||
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := len(data) - 1; i >= 0; i-- {
|
||||
if err := wc.WriteMessage(TextMessage, data[:i]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
messageCount++
|
||||
}
|
||||
|
||||
// Whitespace.
|
||||
|
||||
if err := wc.WriteMessage(TextMessage, []byte(" ")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
messageCount++
|
||||
|
||||
// Close.
|
||||
|
||||
if err := wc.WriteMessage(CloseMessage, FormatCloseMessage(CloseNormalClosure, "")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < messageCount; i++ {
|
||||
err := rc.ReadJSON(&v)
|
||||
if err != io.ErrUnexpectedEOF {
|
||||
t.Error("read", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = rc.ReadJSON(&v)
|
||||
if _, ok := err.(*CloseError); !ok {
|
||||
t.Error("final", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeprecatedJSON(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
wc := newTestConn(nil, &buf, true)
|
||||
rc := newTestConn(&buf, nil, false)
|
||||
|
||||
var actual, expect struct {
|
||||
A int
|
||||
B string
|
||||
}
|
||||
expect.A = 1
|
||||
expect.B = "hello"
|
||||
|
||||
if err := WriteJSON(wc, &expect); err != nil {
|
||||
t.Fatal("write", err)
|
||||
}
|
||||
|
||||
if err := ReadJSON(rc, &actual); err != nil {
|
||||
t.Fatal("read", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(&actual, &expect) {
|
||||
t.Fatal("equal", actual, expect)
|
||||
}
|
||||
}
|
||||
55
mask.go
Normal file
55
mask.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in the
|
||||
// LICENSE file.
|
||||
|
||||
//go:build !appengine
|
||||
// +build !appengine
|
||||
|
||||
package websocket
|
||||
|
||||
import "unsafe"
|
||||
|
||||
const wordSize = int(unsafe.Sizeof(uintptr(0)))
|
||||
|
||||
func maskBytes(key [4]byte, pos int, b []byte) int {
|
||||
// Mask one byte at a time for small buffers.
|
||||
if len(b) < 2*wordSize {
|
||||
for i := range b {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
return pos & 3
|
||||
}
|
||||
|
||||
// Mask one byte at a time to word boundary.
|
||||
if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 {
|
||||
n = wordSize - n
|
||||
for i := range b[:n] {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
b = b[n:]
|
||||
}
|
||||
|
||||
// Create aligned word size key.
|
||||
var k [wordSize]byte
|
||||
for i := range k {
|
||||
k[i] = key[(pos+i)&3]
|
||||
}
|
||||
kw := *(*uintptr)(unsafe.Pointer(&k))
|
||||
|
||||
// Mask one word at a time.
|
||||
n := (len(b) / wordSize) * wordSize
|
||||
for i := 0; i < n; i += wordSize {
|
||||
*(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw
|
||||
}
|
||||
|
||||
// Mask one byte at a time for remaining bytes.
|
||||
b = b[n:]
|
||||
for i := range b {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
|
||||
return pos & 3
|
||||
}
|
||||
16
mask_safe.go
Normal file
16
mask_safe.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in the
|
||||
// LICENSE file.
|
||||
|
||||
//go:build appengine
|
||||
// +build appengine
|
||||
|
||||
package websocket
|
||||
|
||||
func maskBytes(key [4]byte, pos int, b []byte) int {
|
||||
for i := range b {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
return pos & 3
|
||||
}
|
||||
72
mask_test.go
Normal file
72
mask_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
|
||||
// this source code is governed by a BSD-style license that can be found in the
|
||||
// LICENSE file.
|
||||
|
||||
// !appengine
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func maskBytesByByte(key [4]byte, pos int, b []byte) int {
|
||||
for i := range b {
|
||||
b[i] ^= key[pos&3]
|
||||
pos++
|
||||
}
|
||||
return pos & 3
|
||||
}
|
||||
|
||||
func notzero(b []byte) int {
|
||||
for i := range b {
|
||||
if b[i] != 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func TestMaskBytes(t *testing.T) {
|
||||
key := [4]byte{1, 2, 3, 4}
|
||||
for size := 1; size <= 1024; size++ {
|
||||
for align := 0; align < wordSize; align++ {
|
||||
for pos := 0; pos < 4; pos++ {
|
||||
b := make([]byte, size+align)[align:]
|
||||
maskBytes(key, pos, b)
|
||||
maskBytesByByte(key, pos, b)
|
||||
if i := notzero(b); i >= 0 {
|
||||
t.Errorf("size:%d, align:%d, pos:%d, offset:%d", size, align, pos, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMaskBytes(b *testing.B) {
|
||||
for _, size := range []int{2, 4, 8, 16, 32, 512, 1024} {
|
||||
b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
|
||||
for _, align := range []int{wordSize / 2} {
|
||||
b.Run(fmt.Sprintf("align-%d", align), func(b *testing.B) {
|
||||
for _, fn := range []struct {
|
||||
name string
|
||||
fn func(key [4]byte, pos int, b []byte) int
|
||||
}{
|
||||
{"byte", maskBytesByByte},
|
||||
{"word", maskBytes},
|
||||
} {
|
||||
b.Run(fn.name, func(b *testing.B) {
|
||||
key := newMaskKey()
|
||||
data := make([]byte, size+align)[align:]
|
||||
for i := 0; i < b.N; i++ {
|
||||
fn.fn(key, 0, data)
|
||||
}
|
||||
b.SetBytes(int64(len(data)))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
102
prepared.go
Normal file
102
prepared.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PreparedMessage caches on the wire representations of a message payload.
|
||||
// Use PreparedMessage to efficiently send a message payload to multiple
|
||||
// connections. PreparedMessage is especially useful when compression is used
|
||||
// because the CPU and memory expensive compression operation can be executed
|
||||
// once for a given set of compression options.
|
||||
type PreparedMessage struct {
|
||||
messageType int
|
||||
data []byte
|
||||
mu sync.Mutex
|
||||
frames map[prepareKey]*preparedFrame
|
||||
}
|
||||
|
||||
// prepareKey defines a unique set of options to cache prepared frames in PreparedMessage.
|
||||
type prepareKey struct {
|
||||
isServer bool
|
||||
compress bool
|
||||
compressionLevel int
|
||||
}
|
||||
|
||||
// preparedFrame contains data in wire representation.
|
||||
type preparedFrame struct {
|
||||
once sync.Once
|
||||
data []byte
|
||||
}
|
||||
|
||||
// NewPreparedMessage returns an initialized PreparedMessage. You can then send
|
||||
// it to connection using WritePreparedMessage method. Valid wire
|
||||
// representation will be calculated lazily only once for a set of current
|
||||
// connection options.
|
||||
func NewPreparedMessage(messageType int, data []byte) (*PreparedMessage, error) {
|
||||
pm := &PreparedMessage{
|
||||
messageType: messageType,
|
||||
frames: make(map[prepareKey]*preparedFrame),
|
||||
data: data,
|
||||
}
|
||||
|
||||
// Prepare a plain server frame.
|
||||
_, frameData, err := pm.frame(prepareKey{isServer: true, compress: false})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// To protect against caller modifying the data argument, remember the data
|
||||
// copied to the plain server frame.
|
||||
pm.data = frameData[len(frameData)-len(data):]
|
||||
return pm, nil
|
||||
}
|
||||
|
||||
func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) {
|
||||
pm.mu.Lock()
|
||||
frame, ok := pm.frames[key]
|
||||
if !ok {
|
||||
frame = &preparedFrame{}
|
||||
pm.frames[key] = frame
|
||||
}
|
||||
pm.mu.Unlock()
|
||||
|
||||
var err error
|
||||
frame.once.Do(func() {
|
||||
// Prepare a frame using a 'fake' connection.
|
||||
// TODO: Refactor code in conn.go to allow more direct construction of
|
||||
// the frame.
|
||||
mu := make(chan struct{}, 1)
|
||||
mu <- struct{}{}
|
||||
var nc prepareConn
|
||||
c := &Conn{
|
||||
conn: &nc,
|
||||
mu: mu,
|
||||
isServer: key.isServer,
|
||||
compressionLevel: key.compressionLevel,
|
||||
enableWriteCompression: true,
|
||||
writeBuf: make([]byte, defaultWriteBufferSize+maxFrameHeaderSize),
|
||||
}
|
||||
if key.compress {
|
||||
c.newCompressionWriter = compressNoContextTakeover
|
||||
}
|
||||
err = c.WriteMessage(pm.messageType, pm.data)
|
||||
frame.data = nc.buf.Bytes()
|
||||
})
|
||||
return pm.messageType, frame.data, err
|
||||
}
|
||||
|
||||
type prepareConn struct {
|
||||
buf bytes.Buffer
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (pc *prepareConn) Write(p []byte) (int, error) { return pc.buf.Write(p) }
|
||||
func (pc *prepareConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
81
prepared_test.go
Normal file
81
prepared_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/flate"
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var preparedMessageTests = []struct {
|
||||
messageType int
|
||||
isServer bool
|
||||
enableWriteCompression bool
|
||||
compressionLevel int
|
||||
}{
|
||||
// Server
|
||||
{TextMessage, true, false, flate.BestSpeed},
|
||||
{TextMessage, true, true, flate.BestSpeed},
|
||||
{TextMessage, true, true, flate.BestCompression},
|
||||
{PingMessage, true, false, flate.BestSpeed},
|
||||
{PingMessage, true, true, flate.BestSpeed},
|
||||
|
||||
// Client
|
||||
{TextMessage, false, false, flate.BestSpeed},
|
||||
{TextMessage, false, true, flate.BestSpeed},
|
||||
{TextMessage, false, true, flate.BestCompression},
|
||||
{PingMessage, false, false, flate.BestSpeed},
|
||||
{PingMessage, false, true, flate.BestSpeed},
|
||||
}
|
||||
|
||||
func TestPreparedMessage(t *testing.T) {
|
||||
testRand := rand.New(rand.NewSource(99))
|
||||
prevMaskRand := maskRand
|
||||
maskRand = testRand
|
||||
defer func() { maskRand = prevMaskRand }()
|
||||
|
||||
for _, tt := range preparedMessageTests {
|
||||
var data = []byte("this is a test")
|
||||
var buf bytes.Buffer
|
||||
c := newTestConn(nil, &buf, tt.isServer)
|
||||
if tt.enableWriteCompression {
|
||||
c.newCompressionWriter = compressNoContextTakeover
|
||||
}
|
||||
if err := c.SetCompressionLevel(tt.compressionLevel); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Seed random number generator for consistent frame mask.
|
||||
testRand.Seed(1234)
|
||||
|
||||
if err := c.WriteMessage(tt.messageType, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := buf.String()
|
||||
|
||||
pm, err := NewPreparedMessage(tt.messageType, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Scribble on data to ensure that NewPreparedMessage takes a snapshot.
|
||||
copy(data, "hello world")
|
||||
|
||||
// Seed random number generator for consistent frame mask.
|
||||
testRand.Seed(1234)
|
||||
|
||||
buf.Reset()
|
||||
if err := c.WritePreparedMessage(pm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := buf.String()
|
||||
|
||||
if got != want {
|
||||
t.Errorf("write message != prepared message for %+v", tt)
|
||||
}
|
||||
}
|
||||
}
|
||||
104
proxy.go
Normal file
104
proxy.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
type netDialerFunc func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
|
||||
return fn(context.Background(), network, addr)
|
||||
}
|
||||
|
||||
func (fn netDialerFunc) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return fn(ctx, network, addr)
|
||||
}
|
||||
|
||||
func proxyFromURL(proxyURL *url.URL, forwardDial netDialerFunc) (netDialerFunc, error) {
|
||||
if proxyURL.Scheme == "http" || proxyURL.Scheme == "https" {
|
||||
return (&httpProxyDialer{proxyURL: proxyURL, forwardDial: forwardDial}).DialContext, nil
|
||||
}
|
||||
dialer, err := proxy.FromURL(proxyURL, forwardDial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d, ok := dialer.(proxy.ContextDialer); ok {
|
||||
return d.DialContext, nil
|
||||
}
|
||||
return func(ctx context.Context, net, addr string) (net.Conn, error) {
|
||||
return dialer.Dial(net, addr)
|
||||
}, nil
|
||||
}
|
||||
|
||||
type httpProxyDialer struct {
|
||||
proxyURL *url.URL
|
||||
forwardDial netDialerFunc
|
||||
}
|
||||
|
||||
func (hpd *httpProxyDialer) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) {
|
||||
hostPort, _ := hostPortNoPort(hpd.proxyURL)
|
||||
conn, err := hpd.forwardDial(ctx, network, hostPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connectHeader := make(http.Header)
|
||||
if user := hpd.proxyURL.User; user != nil {
|
||||
proxyUser := user.Username()
|
||||
if proxyPassword, passwordSet := user.Password(); passwordSet {
|
||||
credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
|
||||
connectHeader.Set("Proxy-Authorization", "Basic "+credential)
|
||||
}
|
||||
}
|
||||
connectReq := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Opaque: addr},
|
||||
Host: addr,
|
||||
Header: connectHeader,
|
||||
}
|
||||
|
||||
if err := connectReq.Write(conn); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read response. It's OK to use and discard buffered reader here because
|
||||
// the remote server does not speak until spoken to.
|
||||
br := bufio.NewReader(conn)
|
||||
resp, err := http.ReadResponse(br, connectReq)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Close the response body to silence false positives from linters. Reset
|
||||
// the buffered reader first to ensure that Close() does not read from
|
||||
// conn.
|
||||
// Note: Applications must call resp.Body.Close() on a response returned
|
||||
// http.ReadResponse to inspect trailers or read another response from the
|
||||
// buffered reader. The call to resp.Body.Close() does not release
|
||||
// resources.
|
||||
br.Reset(bytes.NewReader(nil))
|
||||
_ = resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
_ = conn.Close()
|
||||
f := strings.SplitN(resp.Status, " ", 2)
|
||||
return nil, errors.New(f[1])
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
373
server.go
Normal file
373
server.go
Normal file
@@ -0,0 +1,373 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HandshakeError describes an error with the handshake from the peer.
|
||||
type HandshakeError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e HandshakeError) Error() string { return e.message }
|
||||
|
||||
// Upgrader specifies parameters for upgrading an HTTP connection to a
|
||||
// WebSocket connection.
|
||||
//
|
||||
// It is safe to call Upgrader's methods concurrently.
|
||||
type Upgrader struct {
|
||||
// HandshakeTimeout specifies the duration for the handshake to complete.
|
||||
HandshakeTimeout time.Duration
|
||||
|
||||
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
|
||||
// size is zero, then buffers allocated by the HTTP server are used. The
|
||||
// I/O buffer sizes do not limit the size of the messages that can be sent
|
||||
// or received.
|
||||
ReadBufferSize, WriteBufferSize int
|
||||
|
||||
// WriteBufferPool is a pool of buffers for write operations. If the value
|
||||
// is not set, then write buffers are allocated to the connection for the
|
||||
// lifetime of the connection.
|
||||
//
|
||||
// A pool is most useful when the application has a modest volume of writes
|
||||
// across a large number of connections.
|
||||
//
|
||||
// Applications should use a single pool for each unique value of
|
||||
// WriteBufferSize.
|
||||
WriteBufferPool BufferPool
|
||||
|
||||
// Subprotocols specifies the server's supported protocols in order of
|
||||
// preference. If this field is not nil, then the Upgrade method negotiates a
|
||||
// subprotocol by selecting the first match in this list with a protocol
|
||||
// requested by the client. If there's no match, then no protocol is
|
||||
// negotiated (the Sec-Websocket-Protocol header is not included in the
|
||||
// handshake response).
|
||||
Subprotocols []string
|
||||
|
||||
// Error specifies the function for generating HTTP error responses. If Error
|
||||
// is nil, then http.Error is used to generate the HTTP response.
|
||||
Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
|
||||
|
||||
// CheckOrigin returns true if the request Origin header is acceptable. If
|
||||
// CheckOrigin is nil, then a safe default is used: return false if the
|
||||
// Origin request header is present and the origin host is not equal to
|
||||
// request Host header.
|
||||
//
|
||||
// A CheckOrigin function should carefully validate the request origin to
|
||||
// prevent cross-site request forgery.
|
||||
CheckOrigin func(r *http.Request) bool
|
||||
|
||||
// EnableCompression specify if the server should attempt to negotiate per
|
||||
// message compression (RFC 7692). Setting this value to true does not
|
||||
// guarantee that compression will be supported. Currently only "no context
|
||||
// takeover" modes are supported.
|
||||
EnableCompression bool
|
||||
}
|
||||
|
||||
func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) {
|
||||
err := HandshakeError{reason}
|
||||
if u.Error != nil {
|
||||
u.Error(w, r, status, err)
|
||||
} else {
|
||||
w.Header().Set("Sec-Websocket-Version", "13")
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// checkSameOrigin returns true if the origin is not set or is equal to the request host.
|
||||
func checkSameOrigin(r *http.Request) bool {
|
||||
origin := r.Header["Origin"]
|
||||
if len(origin) == 0 {
|
||||
return true
|
||||
}
|
||||
u, err := url.Parse(origin[0])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return equalASCIIFold(u.Host, r.Host)
|
||||
}
|
||||
|
||||
func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string {
|
||||
if u.Subprotocols != nil {
|
||||
clientProtocols := Subprotocols(r)
|
||||
for _, clientProtocol := range clientProtocols {
|
||||
for _, serverProtocol := range u.Subprotocols {
|
||||
if clientProtocol == serverProtocol {
|
||||
return clientProtocol
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if responseHeader != nil {
|
||||
return responseHeader.Get("Sec-Websocket-Protocol")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
|
||||
//
|
||||
// The responseHeader is included in the response to the client's upgrade
|
||||
// request. Use the responseHeader to specify cookies (Set-Cookie). To specify
|
||||
// subprotocols supported by the server, set Upgrader.Subprotocols directly.
|
||||
//
|
||||
// If the upgrade fails, then Upgrade replies to the client with an HTTP error
|
||||
// response.
|
||||
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
|
||||
const badHandshake = "websocket: the client is not using the websocket protocol: "
|
||||
|
||||
if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
|
||||
return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header")
|
||||
}
|
||||
|
||||
if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
|
||||
w.Header().Set("Upgrade", "websocket")
|
||||
return u.returnError(w, r, http.StatusUpgradeRequired, badHandshake+"'websocket' token not found in 'Upgrade' header")
|
||||
}
|
||||
|
||||
if r.Method != http.MethodGet {
|
||||
return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET")
|
||||
}
|
||||
|
||||
if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {
|
||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header")
|
||||
}
|
||||
|
||||
if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
|
||||
return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported")
|
||||
}
|
||||
|
||||
checkOrigin := u.CheckOrigin
|
||||
if checkOrigin == nil {
|
||||
checkOrigin = checkSameOrigin
|
||||
}
|
||||
if !checkOrigin(r) {
|
||||
return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin")
|
||||
}
|
||||
|
||||
challengeKey := r.Header.Get("Sec-Websocket-Key")
|
||||
if !isValidChallengeKey(challengeKey) {
|
||||
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header must be Base64 encoded value of 16-byte in length")
|
||||
}
|
||||
|
||||
subprotocol := u.selectSubprotocol(r, responseHeader)
|
||||
|
||||
// Negotiate PMCE
|
||||
var compress bool
|
||||
if u.EnableCompression {
|
||||
for _, ext := range parseExtensions(r.Header) {
|
||||
if ext[""] != "permessage-deflate" {
|
||||
continue
|
||||
}
|
||||
compress = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
netConn, brw, err := http.NewResponseController(w).Hijack()
|
||||
if err != nil {
|
||||
return u.returnError(w, r, http.StatusInternalServerError,
|
||||
"websocket: hijack: "+err.Error())
|
||||
}
|
||||
|
||||
// Close the network connection when returning an error. The variable
|
||||
// netConn is set to nil before the success return at the end of the
|
||||
// function.
|
||||
defer func() {
|
||||
if netConn != nil {
|
||||
// It's safe to ignore the error from Close() because this code is
|
||||
// only executed when returning a more important error to the
|
||||
// application.
|
||||
_ = netConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
var br *bufio.Reader
|
||||
if u.ReadBufferSize == 0 && brw.Reader.Size() > 256 {
|
||||
// Use hijacked buffered reader as the connection reader.
|
||||
br = brw.Reader
|
||||
} else if brw.Reader.Buffered() > 0 {
|
||||
// Wrap the network connection to read buffered data in brw.Reader
|
||||
// before reading from the network connection. This should be rare
|
||||
// because a client must not send message data before receiving the
|
||||
// handshake response.
|
||||
netConn = &brNetConn{br: brw.Reader, Conn: netConn}
|
||||
}
|
||||
|
||||
buf := brw.Writer.AvailableBuffer()
|
||||
|
||||
var writeBuf []byte
|
||||
if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
|
||||
// Reuse hijacked write buffer as connection buffer.
|
||||
writeBuf = buf
|
||||
}
|
||||
|
||||
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)
|
||||
c.subprotocol = subprotocol
|
||||
|
||||
if compress {
|
||||
c.newCompressionWriter = compressNoContextTakeover
|
||||
c.newDecompressionReader = decompressNoContextTakeover
|
||||
}
|
||||
|
||||
// Use larger of hijacked buffer and connection write buffer for header.
|
||||
p := buf
|
||||
if len(c.writeBuf) > len(p) {
|
||||
p = c.writeBuf
|
||||
}
|
||||
p = p[:0]
|
||||
|
||||
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
|
||||
p = append(p, computeAcceptKey(challengeKey)...)
|
||||
p = append(p, "\r\n"...)
|
||||
if c.subprotocol != "" {
|
||||
p = append(p, "Sec-WebSocket-Protocol: "...)
|
||||
p = append(p, c.subprotocol...)
|
||||
p = append(p, "\r\n"...)
|
||||
}
|
||||
if compress {
|
||||
p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
|
||||
}
|
||||
for k, vs := range responseHeader {
|
||||
if k == "Sec-Websocket-Protocol" {
|
||||
continue
|
||||
}
|
||||
for _, v := range vs {
|
||||
p = append(p, k...)
|
||||
p = append(p, ": "...)
|
||||
for i := 0; i < len(v); i++ {
|
||||
b := v[i]
|
||||
if b <= 31 {
|
||||
// prevent response splitting.
|
||||
b = ' '
|
||||
}
|
||||
p = append(p, b)
|
||||
}
|
||||
p = append(p, "\r\n"...)
|
||||
}
|
||||
}
|
||||
p = append(p, "\r\n"...)
|
||||
|
||||
if u.HandshakeTimeout > 0 {
|
||||
if err := netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Clear deadlines set by HTTP server.
|
||||
if err := netConn.SetDeadline(time.Time{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = netConn.Write(p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.HandshakeTimeout > 0 {
|
||||
if err := netConn.SetWriteDeadline(time.Time{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Success! Set netConn to nil to stop the deferred function above from
|
||||
// closing the network connection.
|
||||
netConn = nil
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
|
||||
//
|
||||
// Deprecated: Use websocket.Upgrader instead.
|
||||
//
|
||||
// Upgrade does not perform origin checking. The application is responsible for
|
||||
// checking the Origin header before calling Upgrade. An example implementation
|
||||
// of the same origin policy check is:
|
||||
//
|
||||
// if req.Header.Get("Origin") != "http://"+req.Host {
|
||||
// http.Error(w, "Origin not allowed", http.StatusForbidden)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// If the endpoint supports subprotocols, then the application is responsible
|
||||
// for negotiating the protocol used on the connection. Use the Subprotocols()
|
||||
// function to get the subprotocols requested by the client. Use the
|
||||
// Sec-Websocket-Protocol response header to specify the subprotocol selected
|
||||
// by the application.
|
||||
//
|
||||
// The responseHeader is included in the response to the client's upgrade
|
||||
// request. Use the responseHeader to specify cookies (Set-Cookie) and the
|
||||
// negotiated subprotocol (Sec-Websocket-Protocol).
|
||||
//
|
||||
// The connection buffers IO to the underlying network connection. The
|
||||
// readBufSize and writeBufSize parameters specify the size of the buffers to
|
||||
// use. Messages can be larger than the buffers.
|
||||
//
|
||||
// If the request is not a valid WebSocket handshake, then Upgrade returns an
|
||||
// error of type HandshakeError. Applications should handle this error by
|
||||
// replying to the client with an HTTP error response.
|
||||
func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) {
|
||||
u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize}
|
||||
u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) {
|
||||
// don't return errors to maintain backwards compatibility
|
||||
}
|
||||
u.CheckOrigin = func(r *http.Request) bool {
|
||||
// allow all connections by default
|
||||
return true
|
||||
}
|
||||
return u.Upgrade(w, r, responseHeader)
|
||||
}
|
||||
|
||||
// Subprotocols returns the subprotocols requested by the client in the
|
||||
// Sec-Websocket-Protocol header.
|
||||
func Subprotocols(r *http.Request) []string {
|
||||
h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol"))
|
||||
if h == "" {
|
||||
return nil
|
||||
}
|
||||
protocols := strings.Split(h, ",")
|
||||
for i := range protocols {
|
||||
protocols[i] = strings.TrimSpace(protocols[i])
|
||||
}
|
||||
return protocols
|
||||
}
|
||||
|
||||
// IsWebSocketUpgrade returns true if the client requested upgrade to the
|
||||
// WebSocket protocol.
|
||||
func IsWebSocketUpgrade(r *http.Request) bool {
|
||||
return tokenListContainsValue(r.Header, "Connection", "upgrade") &&
|
||||
tokenListContainsValue(r.Header, "Upgrade", "websocket")
|
||||
}
|
||||
|
||||
type brNetConn struct {
|
||||
br *bufio.Reader
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (b *brNetConn) Read(p []byte) (n int, err error) {
|
||||
if b.br != nil {
|
||||
// Limit read to buferred data.
|
||||
if n := b.br.Buffered(); len(p) > n {
|
||||
p = p[:n]
|
||||
}
|
||||
n, err = b.br.Read(p)
|
||||
if b.br.Buffered() == 0 {
|
||||
b.br = nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
return b.Conn.Read(p)
|
||||
}
|
||||
|
||||
// NetConn returns the underlying connection that is wrapped by b.
|
||||
func (b *brNetConn) NetConn() net.Conn {
|
||||
return b.Conn
|
||||
}
|
||||
|
||||
171
server_test.go
Normal file
171
server_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var subprotocolTests = []struct {
|
||||
h string
|
||||
protocols []string
|
||||
}{
|
||||
{"", nil},
|
||||
{"foo", []string{"foo"}},
|
||||
{"foo,bar", []string{"foo", "bar"}},
|
||||
{"foo, bar", []string{"foo", "bar"}},
|
||||
{" foo, bar", []string{"foo", "bar"}},
|
||||
{" foo, bar ", []string{"foo", "bar"}},
|
||||
}
|
||||
|
||||
func TestSubprotocols(t *testing.T) {
|
||||
for _, st := range subprotocolTests {
|
||||
r := http.Request{Header: http.Header{"Sec-Websocket-Protocol": {st.h}}}
|
||||
protocols := Subprotocols(&r)
|
||||
if !reflect.DeepEqual(st.protocols, protocols) {
|
||||
t.Errorf("SubProtocols(%q) returned %#v, want %#v", st.h, protocols, st.protocols)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isWebSocketUpgradeTests = []struct {
|
||||
ok bool
|
||||
h http.Header
|
||||
}{
|
||||
{false, http.Header{"Upgrade": {"websocket"}}},
|
||||
{false, http.Header{"Connection": {"upgrade"}}},
|
||||
{true, http.Header{"Connection": {"upgRade"}, "Upgrade": {"WebSocket"}}},
|
||||
}
|
||||
|
||||
func TestIsWebSocketUpgrade(t *testing.T) {
|
||||
for _, tt := range isWebSocketUpgradeTests {
|
||||
ok := IsWebSocketUpgrade(&http.Request{Header: tt.h})
|
||||
if tt.ok != ok {
|
||||
t.Errorf("IsWebSocketUpgrade(%v) returned %v, want %v", tt.h, ok, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubProtocolSelection(t *testing.T) {
|
||||
upgrader := Upgrader{
|
||||
Subprotocols: []string{"foo", "bar", "baz"},
|
||||
}
|
||||
|
||||
r := http.Request{Header: http.Header{"Sec-Websocket-Protocol": {"foo", "bar"}}}
|
||||
s := upgrader.selectSubprotocol(&r, nil)
|
||||
if s != "foo" {
|
||||
t.Errorf("Upgrader.selectSubprotocol returned %v, want %v", s, "foo")
|
||||
}
|
||||
|
||||
r = http.Request{Header: http.Header{"Sec-Websocket-Protocol": {"bar", "foo"}}}
|
||||
s = upgrader.selectSubprotocol(&r, nil)
|
||||
if s != "bar" {
|
||||
t.Errorf("Upgrader.selectSubprotocol returned %v, want %v", s, "bar")
|
||||
}
|
||||
|
||||
r = http.Request{Header: http.Header{"Sec-Websocket-Protocol": {"baz"}}}
|
||||
s = upgrader.selectSubprotocol(&r, nil)
|
||||
if s != "baz" {
|
||||
t.Errorf("Upgrader.selectSubprotocol returned %v, want %v", s, "baz")
|
||||
}
|
||||
|
||||
r = http.Request{Header: http.Header{"Sec-Websocket-Protocol": {"quux"}}}
|
||||
s = upgrader.selectSubprotocol(&r, nil)
|
||||
if s != "" {
|
||||
t.Errorf("Upgrader.selectSubprotocol returned %v, want %v", s, "empty string")
|
||||
}
|
||||
}
|
||||
|
||||
var checkSameOriginTests = []struct {
|
||||
ok bool
|
||||
r *http.Request
|
||||
}{
|
||||
{false, &http.Request{Host: "example.org", Header: map[string][]string{"Origin": {"https://other.org"}}}},
|
||||
{true, &http.Request{Host: "example.org", Header: map[string][]string{"Origin": {"https://example.org"}}}},
|
||||
{true, &http.Request{Host: "Example.org", Header: map[string][]string{"Origin": {"https://example.org"}}}},
|
||||
}
|
||||
|
||||
func TestCheckSameOrigin(t *testing.T) {
|
||||
for _, tt := range checkSameOriginTests {
|
||||
ok := checkSameOrigin(tt.r)
|
||||
if tt.ok != ok {
|
||||
t.Errorf("checkSameOrigin(%+v) returned %v, want %v", tt.r, ok, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type reuseTestResponseWriter struct {
|
||||
brw *bufio.ReadWriter
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (resp *reuseTestResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
return fakeNetConn{strings.NewReader(""), &bytes.Buffer{}}, resp.brw, nil
|
||||
}
|
||||
|
||||
var bufioReuseTests = []struct {
|
||||
n int
|
||||
reuse bool
|
||||
}{
|
||||
{4096, true},
|
||||
{128, false},
|
||||
}
|
||||
|
||||
func xTestBufioReuse(t *testing.T) {
|
||||
for i, tt := range bufioReuseTests {
|
||||
br := bufio.NewReaderSize(strings.NewReader(""), tt.n)
|
||||
bw := bufio.NewWriterSize(&bytes.Buffer{}, tt.n)
|
||||
resp := &reuseTestResponseWriter{
|
||||
brw: bufio.NewReadWriter(br, bw),
|
||||
}
|
||||
upgrader := Upgrader{}
|
||||
c, err := upgrader.Upgrade(resp, &http.Request{
|
||||
Method: http.MethodGet,
|
||||
Header: http.Header{
|
||||
"Upgrade": []string{"websocket"},
|
||||
"Connection": []string{"upgrade"},
|
||||
"Sec-Websocket-Key": []string{"dGhlIHNhbXBsZSBub25jZQ=="},
|
||||
"Sec-Websocket-Version": []string{"13"},
|
||||
}}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if reuse := c.br == br; reuse != tt.reuse {
|
||||
t.Errorf("%d: buffered reader reuse=%v, want %v", i, reuse, tt.reuse)
|
||||
}
|
||||
writeBuf := bw.AvailableBuffer()
|
||||
if reuse := &c.writeBuf[0] == &writeBuf[0]; reuse != tt.reuse {
|
||||
t.Errorf("%d: write buffer reuse=%v, want %v", i, reuse, tt.reuse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHijack_NotSupported(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
req.Header.Set("Upgrade", "websocket")
|
||||
req.Header.Set("Connection", "upgrade")
|
||||
req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
||||
req.Header.Set("Sec-Websocket-Version", "13")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
upgrader := Upgrader{}
|
||||
_, err := upgrader.Upgrade(recorder, req, nil)
|
||||
|
||||
if want := (HandshakeError{}); !errors.As(err, &want) || recorder.Code != http.StatusInternalServerError {
|
||||
t.Errorf("want %T and status_code=%d", want, http.StatusInternalServerError)
|
||||
t.Fatalf("got err=%T and status_code=%d", err, recorder.Code)
|
||||
}
|
||||
}
|
||||
298
util.go
Normal file
298
util.go
Normal file
@@ -0,0 +1,298 @@
|
||||
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
|
||||
|
||||
func computeAcceptKey(challengeKey string) string {
|
||||
h := sha1.New()
|
||||
h.Write([]byte(challengeKey))
|
||||
h.Write(keyGUID)
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func generateChallengeKey() (string, error) {
|
||||
p := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, p); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(p), nil
|
||||
}
|
||||
|
||||
// Token octets per RFC 2616.
|
||||
var isTokenOctet = [256]bool{
|
||||
'!': true,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': true,
|
||||
'\'': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'W': true,
|
||||
'V': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'|': true,
|
||||
'~': true,
|
||||
}
|
||||
|
||||
// skipSpace returns a slice of the string s with all leading RFC 2616 linear
|
||||
// whitespace removed.
|
||||
func skipSpace(s string) (rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if b := s[i]; b != ' ' && b != '\t' {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[i:]
|
||||
}
|
||||
|
||||
// nextToken returns the leading RFC 2616 token of s and the string following
|
||||
// the token.
|
||||
func nextToken(s string) (token, rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if !isTokenOctet[s[i]] {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[:i], s[i:]
|
||||
}
|
||||
|
||||
// nextTokenOrQuoted returns the leading token or quoted string per RFC 2616
|
||||
// and the string following the token or quoted string.
|
||||
func nextTokenOrQuoted(s string) (value string, rest string) {
|
||||
if !strings.HasPrefix(s, "\"") {
|
||||
return nextToken(s)
|
||||
}
|
||||
s = s[1:]
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '"':
|
||||
return s[:i], s[i+1:]
|
||||
case '\\':
|
||||
p := make([]byte, len(s)-1)
|
||||
j := copy(p, s[:i])
|
||||
escape := true
|
||||
for i = i + 1; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch {
|
||||
case escape:
|
||||
escape = false
|
||||
p[j] = b
|
||||
j++
|
||||
case b == '\\':
|
||||
escape = true
|
||||
case b == '"':
|
||||
return string(p[:j]), s[i+1:]
|
||||
default:
|
||||
p[j] = b
|
||||
j++
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// equalASCIIFold returns true if s is equal to t with ASCII case folding as
|
||||
// defined in RFC 4790.
|
||||
func equalASCIIFold(s, t string) bool {
|
||||
for s != "" && t != "" {
|
||||
sr, size := utf8.DecodeRuneInString(s)
|
||||
s = s[size:]
|
||||
tr, size := utf8.DecodeRuneInString(t)
|
||||
t = t[size:]
|
||||
if sr == tr {
|
||||
continue
|
||||
}
|
||||
if 'A' <= sr && sr <= 'Z' {
|
||||
sr = sr + 'a' - 'A'
|
||||
}
|
||||
if 'A' <= tr && tr <= 'Z' {
|
||||
tr = tr + 'a' - 'A'
|
||||
}
|
||||
if sr != tr {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return s == t
|
||||
}
|
||||
|
||||
// tokenListContainsValue returns true if the 1#token header with the given
|
||||
// name contains a token equal to value with ASCII case folding.
|
||||
func tokenListContainsValue(header http.Header, name string, value string) bool {
|
||||
headers:
|
||||
for _, s := range header[name] {
|
||||
for {
|
||||
var t string
|
||||
t, s = nextToken(skipSpace(s))
|
||||
if t == "" {
|
||||
continue headers
|
||||
}
|
||||
s = skipSpace(s)
|
||||
if s != "" && s[0] != ',' {
|
||||
continue headers
|
||||
}
|
||||
if equalASCIIFold(t, value) {
|
||||
return true
|
||||
}
|
||||
if s == "" {
|
||||
continue headers
|
||||
}
|
||||
s = s[1:]
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseExtensions parses WebSocket extensions from a header.
|
||||
func parseExtensions(header http.Header) []map[string]string {
|
||||
// From RFC 6455:
|
||||
//
|
||||
// Sec-WebSocket-Extensions = extension-list
|
||||
// extension-list = 1#extension
|
||||
// extension = extension-token *( ";" extension-param )
|
||||
// extension-token = registered-token
|
||||
// registered-token = token
|
||||
// extension-param = token [ "=" (token | quoted-string) ]
|
||||
// ;When using the quoted-string syntax variant, the value
|
||||
// ;after quoted-string unescaping MUST conform to the
|
||||
// ;'token' ABNF.
|
||||
|
||||
var result []map[string]string
|
||||
headers:
|
||||
for _, s := range header["Sec-Websocket-Extensions"] {
|
||||
for {
|
||||
var t string
|
||||
t, s = nextToken(skipSpace(s))
|
||||
if t == "" {
|
||||
continue headers
|
||||
}
|
||||
ext := map[string]string{"": t}
|
||||
for {
|
||||
s = skipSpace(s)
|
||||
if !strings.HasPrefix(s, ";") {
|
||||
break
|
||||
}
|
||||
var k string
|
||||
k, s = nextToken(skipSpace(s[1:]))
|
||||
if k == "" {
|
||||
continue headers
|
||||
}
|
||||
s = skipSpace(s)
|
||||
var v string
|
||||
if strings.HasPrefix(s, "=") {
|
||||
v, s = nextTokenOrQuoted(skipSpace(s[1:]))
|
||||
s = skipSpace(s)
|
||||
}
|
||||
if s != "" && s[0] != ',' && s[0] != ';' {
|
||||
continue headers
|
||||
}
|
||||
ext[k] = v
|
||||
}
|
||||
if s != "" && s[0] != ',' {
|
||||
continue headers
|
||||
}
|
||||
result = append(result, ext)
|
||||
if s == "" {
|
||||
continue headers
|
||||
}
|
||||
s = s[1:]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// isValidChallengeKey checks if the argument meets RFC6455 specification.
|
||||
func isValidChallengeKey(s string) bool {
|
||||
// From RFC6455:
|
||||
//
|
||||
// A |Sec-WebSocket-Key| header field with a base64-encoded (see
|
||||
// Section 4 of [RFC4648]) value that, when decoded, is 16 bytes in
|
||||
// length.
|
||||
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(s)
|
||||
return err == nil && len(decoded) == 16
|
||||
}
|
||||
115
util_test.go
Normal file
115
util_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright 2014 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var equalASCIIFoldTests = []struct {
|
||||
t, s string
|
||||
eq bool
|
||||
}{
|
||||
{"WebSocket", "websocket", true},
|
||||
{"websocket", "WebSocket", true},
|
||||
{"Öyster", "öyster", false},
|
||||
{"WebSocket", "WetSocket", false},
|
||||
}
|
||||
|
||||
func TestEqualASCIIFold(t *testing.T) {
|
||||
for _, tt := range equalASCIIFoldTests {
|
||||
eq := equalASCIIFold(tt.s, tt.t)
|
||||
if eq != tt.eq {
|
||||
t.Errorf("equalASCIIFold(%q, %q) = %v, want %v", tt.s, tt.t, eq, tt.eq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tokenListContainsValueTests = []struct {
|
||||
value string
|
||||
ok bool
|
||||
}{
|
||||
{"WebSocket", true},
|
||||
{"WEBSOCKET", true},
|
||||
{"websocket", true},
|
||||
{"websockets", false},
|
||||
{"x websocket", false},
|
||||
{"websocket x", false},
|
||||
{"other,websocket,more", true},
|
||||
{"other, websocket, more", true},
|
||||
}
|
||||
|
||||
func TestTokenListContainsValue(t *testing.T) {
|
||||
for _, tt := range tokenListContainsValueTests {
|
||||
h := http.Header{"Upgrade": {tt.value}}
|
||||
ok := tokenListContainsValue(h, "Upgrade", "websocket")
|
||||
if ok != tt.ok {
|
||||
t.Errorf("tokenListContainsValue(h, n, %q) = %v, want %v", tt.value, ok, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isValidChallengeKeyTests = []struct {
|
||||
key string
|
||||
ok bool
|
||||
}{
|
||||
{"dGhlIHNhbXBsZSBub25jZQ==", true},
|
||||
{"", false},
|
||||
{"InvalidKey", false},
|
||||
{"WHQ4eXhscUtKYjBvOGN3WEdtOEQ=", false},
|
||||
}
|
||||
|
||||
func TestIsValidChallengeKey(t *testing.T) {
|
||||
for _, tt := range isValidChallengeKeyTests {
|
||||
ok := isValidChallengeKey(tt.key)
|
||||
if ok != tt.ok {
|
||||
t.Errorf("isValidChallengeKey returns %v, want %v", ok, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var parseExtensionTests = []struct {
|
||||
value string
|
||||
extensions []map[string]string
|
||||
}{
|
||||
{`foo`, []map[string]string{{"": "foo"}}},
|
||||
{`foo, bar; baz=2`, []map[string]string{
|
||||
{"": "foo"},
|
||||
{"": "bar", "baz": "2"}}},
|
||||
{`foo; bar="b,a;z"`, []map[string]string{
|
||||
{"": "foo", "bar": "b,a;z"}}},
|
||||
{`foo , bar; baz = 2`, []map[string]string{
|
||||
{"": "foo"},
|
||||
{"": "bar", "baz": "2"}}},
|
||||
{`foo, bar; baz=2 junk`, []map[string]string{
|
||||
{"": "foo"}}},
|
||||
{`foo junk, bar; baz=2 junk`, nil},
|
||||
{`mux; max-channels=4; flow-control, deflate-stream`, []map[string]string{
|
||||
{"": "mux", "max-channels": "4", "flow-control": ""},
|
||||
{"": "deflate-stream"}}},
|
||||
{`permessage-foo; x="10"`, []map[string]string{
|
||||
{"": "permessage-foo", "x": "10"}}},
|
||||
{`permessage-foo; use_y, permessage-foo`, []map[string]string{
|
||||
{"": "permessage-foo", "use_y": ""},
|
||||
{"": "permessage-foo"}}},
|
||||
{`permessage-deflate; client_max_window_bits; server_max_window_bits=10 , permessage-deflate; client_max_window_bits`, []map[string]string{
|
||||
{"": "permessage-deflate", "client_max_window_bits": "", "server_max_window_bits": "10"},
|
||||
{"": "permessage-deflate", "client_max_window_bits": ""}}},
|
||||
{"permessage-deflate; server_no_context_takeover; client_max_window_bits=15", []map[string]string{
|
||||
{"": "permessage-deflate", "server_no_context_takeover": "", "client_max_window_bits": "15"},
|
||||
}},
|
||||
}
|
||||
|
||||
func TestParseExtensions(t *testing.T) {
|
||||
for _, tt := range parseExtensionTests {
|
||||
h := http.Header{http.CanonicalHeaderKey("Sec-WebSocket-Extensions"): {tt.value}}
|
||||
extensions := parseExtensions(h)
|
||||
if !reflect.DeepEqual(extensions, tt.extensions) {
|
||||
t.Errorf("parseExtensions(%q)\n = %v,\nwant %v", tt.value, extensions, tt.extensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user