ing
This commit is contained in:
parent
c1de188784
commit
67de428f46
|
@ -1,26 +0,0 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type BackendClient interface {
|
||||
Exec(ctx context.Context, in *BackendClientInput, opts ...grpc.CallOption) (*BackendClientOutput, error)
|
||||
}
|
||||
|
||||
type BackendClientInput struct {
|
||||
Target string
|
||||
Method string
|
||||
Params []string
|
||||
}
|
||||
|
||||
type BackendClientOutput struct {
|
||||
Result string
|
||||
}
|
||||
|
||||
type PooledClient interface {
|
||||
Exec(ctx context.Context, target string, method string, params []string) (string, error)
|
||||
Close()
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// Dial is an application supplied function for creating and configuring a
|
||||
// grpc connection.
|
||||
//
|
||||
// The grpc.ClientConn returned from Dial
|
||||
Dial func() (*grpc.ClientConn, error)
|
||||
|
||||
// NewClient is an application supplied function for creating and configuring a
|
||||
// client.
|
||||
//
|
||||
// The client returned from NewClient
|
||||
NewClient func(*grpc.ClientConn) (BackendClient, error)
|
||||
|
||||
// Initial number of clients in the pool.
|
||||
InitCapacity int
|
||||
|
||||
// Maximum number of clients allocated by the pool at a given time.
|
||||
// When zero, there is no limit on the number of clients in the pool.
|
||||
MaxCapacity int
|
||||
}
|
153
backend/pool.go
153
backend/pool.go
|
@ -1,153 +0,0 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type Pool interface {
|
||||
// Get gets a client. The application must close the returned client.
|
||||
// This method always returns a valid client so that applications can defer
|
||||
// error handling to the first use of the client. If there is an error
|
||||
// getting an underlying client, then the client Err, Do, Send, Flush
|
||||
// and Receive methods return that error.
|
||||
Get() (PooledClient, error)
|
||||
|
||||
// Capacity returns the number of maximum clients in the pool.
|
||||
Capacity() int
|
||||
|
||||
// Available returns the number of avaliable clients in the pool.
|
||||
Available() int
|
||||
|
||||
// Destroy releases the resources used by the pool.
|
||||
Destroy()
|
||||
}
|
||||
|
||||
type pool struct {
|
||||
dial func() (*grpc.ClientConn, error)
|
||||
newClient func(*grpc.ClientConn) (BackendClient, error)
|
||||
initCapacity int
|
||||
maxCapacity int
|
||||
|
||||
conn *grpc.ClientConn
|
||||
mtx sync.Mutex
|
||||
clients chan PooledClient
|
||||
}
|
||||
|
||||
func NewPool(o Options) (Pool, error) {
|
||||
if o.Dial == nil {
|
||||
return nil, fmt.Errorf("invalid Dial settings")
|
||||
}
|
||||
|
||||
if o.NewClient == nil {
|
||||
return nil, fmt.Errorf("invalid NewClient settings")
|
||||
}
|
||||
|
||||
if o.InitCapacity < 0 || o.MaxCapacity < 0 {
|
||||
return nil, fmt.Errorf("invalid capacity settings")
|
||||
}
|
||||
|
||||
p := &pool{
|
||||
dial: o.Dial,
|
||||
newClient: o.NewClient,
|
||||
initCapacity: o.InitCapacity,
|
||||
maxCapacity: o.MaxCapacity,
|
||||
clients: make(chan PooledClient, o.InitCapacity),
|
||||
}
|
||||
|
||||
var err error
|
||||
p.conn, err = p.dial()
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := 0; i < p.initCapacity; i++ {
|
||||
pc, err := p.create()
|
||||
if err != nil {
|
||||
p.conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
p.clients <- pc
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *pool) Capacity() int {
|
||||
return cap(p.clients)
|
||||
}
|
||||
|
||||
func (p *pool) Available() int {
|
||||
return len(p.clients)
|
||||
}
|
||||
|
||||
func (p *pool) Get() (PooledClient, error) {
|
||||
if p.clients == nil {
|
||||
// pool aleardy destroyed, returns new client
|
||||
return p.create()
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case c := <-p.clients:
|
||||
return c, nil
|
||||
default:
|
||||
return p.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pool) Destroy() {
|
||||
p.mtx.Lock()
|
||||
defer p.mtx.Unlock()
|
||||
if p.clients == nil {
|
||||
// pool aleardy destroyed
|
||||
return
|
||||
}
|
||||
close(p.clients)
|
||||
p.clients = nil
|
||||
p.conn.Close()
|
||||
}
|
||||
|
||||
func (p *pool) create() (PooledClient, error) {
|
||||
c, err := p.newClient(p.conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pc := &pooledClient{
|
||||
p: p,
|
||||
c: c,
|
||||
}
|
||||
return pc, nil
|
||||
}
|
||||
|
||||
type pooledClient struct {
|
||||
p *pool
|
||||
c BackendClient
|
||||
}
|
||||
|
||||
func (pc *pooledClient) Exec(ctx context.Context, target string, method string, params []string) (string, error) {
|
||||
si := &BackendClientInput{
|
||||
Target: target,
|
||||
Method: method,
|
||||
Params: params,
|
||||
}
|
||||
so, err := pc.c.Exec(ctx, si)
|
||||
if nil != err {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return so.Result, nil
|
||||
}
|
||||
|
||||
func (pc *pooledClient) Close() {
|
||||
select {
|
||||
case pc.p.clients <- pc:
|
||||
return
|
||||
default:
|
||||
// pool is full, close passed connection
|
||||
return
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"git.loafle.net/overflow/overflow_gateway_websocket"
|
||||
)
|
||||
|
||||
func main() {
|
||||
s := overflow_gateway_websocket.NewServer()
|
||||
s.OnPreRequest(func() {
|
||||
|
||||
})
|
||||
s.OnPostRequest(func() {
|
||||
|
||||
})
|
||||
|
||||
s.ListenAndServe()
|
||||
}
|
|
@ -12,3 +12,5 @@ import:
|
|||
- package: git.loafle.net/overflow/overflow_api_server
|
||||
subpackages:
|
||||
- golang
|
||||
- package: github.com/satori/go.uuid
|
||||
version: v1.1.0
|
||||
|
|
136
options.go
Normal file
136
options.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
package overflow_gateway_websocket
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.loafle.net/overflow/overflow_gateway_websocket/websocket"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type (
|
||||
OnConnectionFunc func(ctx *fasthttp.RequestCtx, conn *websocket.Conn)
|
||||
OnRequestFunc func(method string, params interface{}) (interface{}, error)
|
||||
OnNotifyFunc func(method string, params interface{})
|
||||
OnPushFunc func()
|
||||
OnErrorFunc func(ctx *fasthttp.RequestCtx, status int, reason error)
|
||||
OnCheckOriginFunc func(ctx *fasthttp.RequestCtx) bool
|
||||
IDGeneratorFunc func(ctx *fasthttp.RequestCtx) string
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultHandshakeTimeout is default value of websocket handshake Timeout
|
||||
DefaultHandshakeTimeout = 0
|
||||
// DefaultWriteTimeout is default value of Write Timeout
|
||||
DefaultWriteTimeout = 0
|
||||
// DefaultReadTimeout is default value of Read Timeout
|
||||
DefaultReadTimeout = 0
|
||||
// DefaultPongTimeout is default value of Pong Timeout
|
||||
DefaultPongTimeout = 60 * time.Second
|
||||
// DefaultPingTimeout is default value of Ping Timeout
|
||||
DefaultPingTimeout = 10 * time.Second
|
||||
// DefaultPingPeriod is default value of Ping Period
|
||||
DefaultPingPeriod = (DefaultPongTimeout * 9) / 10
|
||||
// DefaultMaxMessageSize is default value of Max Message Size
|
||||
DefaultMaxMessageSize = 1024
|
||||
// DefaultReadBufferSize is default value of Read Buffer Size
|
||||
DefaultReadBufferSize = 4096
|
||||
// DefaultWriteBufferSize is default value of Write Buffer Size
|
||||
DefaultWriteBufferSize = 4096
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultIDGenerator returns the UUID of the client
|
||||
DefaultIDGenerator = func(ctx *fasthttp.RequestCtx) string { return uuid.NewV4().String() }
|
||||
)
|
||||
|
||||
// Options is configuration of the websocket server
|
||||
type Options struct {
|
||||
OnConnection OnConnectionFunc
|
||||
OnRequest OnRequestFunc
|
||||
OnNotify OnNotifyFunc
|
||||
OnPush OnPushFunc
|
||||
OnCheckOrigin OnCheckOriginFunc
|
||||
OnError OnErrorFunc
|
||||
IDGenerator IDGeneratorFunc
|
||||
HandshakeTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
PingTimeout time.Duration
|
||||
PongTimeout time.Duration
|
||||
PingPeriod time.Duration
|
||||
MaxMessageSize int64
|
||||
BinaryMessage bool
|
||||
ReadBufferSize int
|
||||
WriteBufferSize int
|
||||
}
|
||||
|
||||
// Validate validates the configuration
|
||||
func (o *Options) Validate() *Options {
|
||||
|
||||
if o.WriteTimeout < 0 {
|
||||
o.WriteTimeout = DefaultWriteTimeout
|
||||
}
|
||||
|
||||
if o.ReadTimeout < 0 {
|
||||
o.ReadTimeout = DefaultReadTimeout
|
||||
}
|
||||
|
||||
if o.PongTimeout < 0 {
|
||||
o.PongTimeout = DefaultPongTimeout
|
||||
}
|
||||
|
||||
if o.PingPeriod <= 0 {
|
||||
o.PingPeriod = DefaultPingPeriod
|
||||
}
|
||||
|
||||
if o.MaxMessageSize <= 0 {
|
||||
o.MaxMessageSize = DefaultMaxMessageSize
|
||||
}
|
||||
|
||||
if o.ReadBufferSize <= 0 {
|
||||
o.ReadBufferSize = DefaultReadBufferSize
|
||||
}
|
||||
|
||||
if o.WriteBufferSize <= 0 {
|
||||
o.WriteBufferSize = DefaultWriteBufferSize
|
||||
}
|
||||
|
||||
if o.OnConnection == nil {
|
||||
o.OnConnection = func(ctx *fasthttp.RequestCtx, conn *websocket.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
if o.OnRequest == nil {
|
||||
o.OnRequest = func(method string, params interface{}) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
if o.OnNotify == nil {
|
||||
o.OnNotify = func(method string, params interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
if o.OnPush == nil {
|
||||
o.OnPush = func() {
|
||||
}
|
||||
}
|
||||
|
||||
if o.OnError == nil {
|
||||
o.OnError = func(ctx *fasthttp.RequestCtx, status int, reason error) {
|
||||
}
|
||||
}
|
||||
|
||||
if o.OnCheckOrigin == nil {
|
||||
o.OnCheckOrigin = func(ctx *fasthttp.RequestCtx) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if o.IDGenerator == nil {
|
||||
o.IDGenerator = DefaultIDGenerator
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package pubsub
|
||||
|
||||
type (
|
||||
OnSubscribeFunc func(channel string, payload string)
|
||||
)
|
||||
|
||||
type PubSub interface {
|
||||
Subscribe(channel string, cb OnSubscribeFunc)
|
||||
}
|
||||
|
||||
type pubSub struct {
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.loafle.net/overflow/overflow_gateway_websocket/pubsub"
|
||||
"github.com/garyburd/redigo/redis"
|
||||
)
|
||||
|
||||
type redisPubSub struct {
|
||||
pool *redis.Pool
|
||||
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func New(addr string) (pubsub.PubSub, error) {
|
||||
r := &redisPubSub{}
|
||||
r.pool = &redis.Pool{
|
||||
MaxIdle: 3,
|
||||
IdleTimeout: 240 * time.Second,
|
||||
Dial: func() (redis.Conn, error) {
|
||||
return redis.Dial("tcp", addr)
|
||||
},
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *redisPubSub) Subscribe(channel string, cb pubsub.OnSubscribeFunc) {
|
||||
conn := r.pool.Get()
|
||||
defer conn.Close()
|
||||
psc := redis.PubSubConn{Conn: conn}
|
||||
psc.Subscribe(channel)
|
||||
for {
|
||||
switch v := psc.Receive().(type) {
|
||||
case redis.Message:
|
||||
log.Printf("message: %s: %s\n", v.Channel, string(v.Data))
|
||||
case redis.Subscription:
|
||||
log.Printf("subscription message: %s: %s %d\n", v.Channel, v.Kind, v.Count)
|
||||
case error:
|
||||
log.Println("error pub/sub, delivery has stopped")
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
105
server.go
105
server.go
|
@ -1,84 +1,65 @@
|
|||
package overflow_gateway_websocket
|
||||
|
||||
import (
|
||||
"git.loafle.net/overflow/overflow_gateway_websocket/backend"
|
||||
"git.loafle.net/overflow/overflow_gateway_websocket/pubsub"
|
||||
"git.loafle.net/overflow/overflow_gateway_websocket/pubsub/redis"
|
||||
"google.golang.org/grpc"
|
||||
"log"
|
||||
|
||||
"git.loafle.net/overflow/overflow_gateway_websocket/websocket"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type (
|
||||
// OnPreRequestFunc is callback function
|
||||
OnPreRequestFunc func()
|
||||
OnPostRequestFunc func()
|
||||
OnPreNotifyFunc func()
|
||||
OnPrePushFunc func()
|
||||
)
|
||||
type ()
|
||||
|
||||
type Server interface {
|
||||
OnPreRequest(cb OnPreRequestFunc)
|
||||
OnPostRequest(cb OnPostRequestFunc)
|
||||
OnPreNotify(cb OnPreNotifyFunc)
|
||||
OnPrePush(cb OnPrePushFunc)
|
||||
ListenAndServe()
|
||||
ListenAndServe(addr string) error
|
||||
}
|
||||
|
||||
type server struct {
|
||||
onPreRequestListeners []OnPreRequestFunc
|
||||
onPostRequestListeners []OnPostRequestFunc
|
||||
onPreNotifyListeners []OnPreNotifyFunc
|
||||
onPrePushListeners []OnPrePushFunc
|
||||
|
||||
backendPool backend.Pool
|
||||
pubSub pubsub.PubSub
|
||||
o *Options
|
||||
upgrader *websocket.Upgrader
|
||||
}
|
||||
|
||||
func NewServer() Server {
|
||||
func NewServer(o *Options) Server {
|
||||
s := &server{
|
||||
onPreRequestListeners: make([]OnPreRequestFunc, 0),
|
||||
onPostRequestListeners: make([]OnPostRequestFunc, 0),
|
||||
onPreNotifyListeners: make([]OnPreNotifyFunc, 0),
|
||||
onPrePushListeners: make([]OnPrePushFunc, 0),
|
||||
o: o.Validate(),
|
||||
}
|
||||
|
||||
var err error
|
||||
s.backendPool, err = backend.NewPool(backend.Options{
|
||||
Dial: func() (*grpc.ClientConn, error) {
|
||||
return nil, nil
|
||||
},
|
||||
NewClient: func(*grpc.ClientConn) (backend.BackendClient, error) {
|
||||
return nil, nil
|
||||
},
|
||||
})
|
||||
|
||||
if nil != err {
|
||||
|
||||
}
|
||||
|
||||
s.pubSub, err = redis.New("")
|
||||
if nil != err {
|
||||
|
||||
s.upgrader = &websocket.Upgrader{
|
||||
ReadBufferSize: s.o.ReadBufferSize,
|
||||
WriteBufferSize: s.o.WriteBufferSize,
|
||||
HandshakeTimeout: s.o.HandshakeTimeout,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *server) OnPreRequest(cb OnPreRequestFunc) {
|
||||
s.onPreRequestListeners = append(s.onPreRequestListeners, cb)
|
||||
}
|
||||
|
||||
func (s *server) OnPostRequest(cb OnPostRequestFunc) {
|
||||
s.onPostRequestListeners = append(s.onPostRequestListeners, cb)
|
||||
}
|
||||
|
||||
func (s *server) OnPreNotify(cb OnPreNotifyFunc) {
|
||||
s.onPreNotifyListeners = append(s.onPreNotifyListeners, cb)
|
||||
}
|
||||
|
||||
func (s *server) OnPrePush(cb OnPrePushFunc) {
|
||||
s.onPrePushListeners = append(s.onPrePushListeners, cb)
|
||||
}
|
||||
|
||||
func (s *server) ListenAndServe() {
|
||||
func (s *server) onRequest(cb OnRequestFunc) {
|
||||
|
||||
}
|
||||
|
||||
func (s *server) onNotify(cb OnNotifyFunc) {
|
||||
|
||||
}
|
||||
|
||||
func (s *server) onPush(cb OnPushFunc) {
|
||||
|
||||
}
|
||||
|
||||
func (s *server) requestHandler(ctx *fasthttp.RequestCtx, c *websocket.Conn) {
|
||||
|
||||
}
|
||||
|
||||
func (s *server) connectionHandler(ctx *fasthttp.RequestCtx) {
|
||||
s.upgrader.Upgrade(ctx, nil, func(conn *websocket.Conn, err error) {
|
||||
if err != nil {
|
||||
log.Print("upgrade:", err)
|
||||
return
|
||||
}
|
||||
s.o.OnConnection(ctx, conn)
|
||||
|
||||
s.requestHandler(ctx, conn)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *server) ListenAndServe(addr string) error {
|
||||
return fasthttp.ListenAndServe(addr, s.connectionHandler)
|
||||
}
|
||||
|
|
392
websocket/client.go
Normal file
392
websocket/client.go
Normal file
|
@ -0,0 +1,392 @@
|
|||
// 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"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"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.
|
||||
type Dialer struct {
|
||||
// NetDial specifies the dial function for creating TCP connections. If
|
||||
// NetDial is nil, net.Dial is used.
|
||||
NetDial func(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.
|
||||
TLSClientConfig *tls.Config
|
||||
|
||||
// HandshakeTimeout specifies the duration for the handshake to complete.
|
||||
HandshakeTimeout time.Duration
|
||||
|
||||
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. 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
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
var errMalformedURL = errors.New("malformed ws or wss URL")
|
||||
|
||||
// parseURL parses the URL.
|
||||
//
|
||||
// This function is a replacement for the standard library url.Parse function.
|
||||
// In Go 1.4 and earlier, url.Parse loses information from the path.
|
||||
func parseURL(s string) (*url.URL, error) {
|
||||
// From the RFC:
|
||||
//
|
||||
// ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
|
||||
// wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
|
||||
var u url.URL
|
||||
switch {
|
||||
case strings.HasPrefix(s, "ws://"):
|
||||
u.Scheme = "ws"
|
||||
s = s[len("ws://"):]
|
||||
case strings.HasPrefix(s, "wss://"):
|
||||
u.Scheme = "wss"
|
||||
s = s[len("wss://"):]
|
||||
default:
|
||||
return nil, errMalformedURL
|
||||
}
|
||||
|
||||
if i := strings.Index(s, "?"); i >= 0 {
|
||||
u.RawQuery = s[i+1:]
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
if i := strings.Index(s, "/"); i >= 0 {
|
||||
u.Opaque = s[i:]
|
||||
s = s[:i]
|
||||
} else {
|
||||
u.Opaque = "/"
|
||||
}
|
||||
|
||||
u.Host = s
|
||||
|
||||
if strings.Contains(u.Host, "@") {
|
||||
// Don't bother parsing user information because user information is
|
||||
// not allowed in websocket URIs.
|
||||
return nil, errMalformedURL
|
||||
}
|
||||
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
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 zero values.
|
||||
var DefaultDialer = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
|
||||
// Dial 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).
|
||||
//
|
||||
// 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) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
|
||||
|
||||
if d == nil {
|
||||
d = &Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
}
|
||||
|
||||
challengeKey, err := generateChallengeKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
u, err := parseURL(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: "GET",
|
||||
URL: u,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: make(http.Header),
|
||||
Host: u.Host,
|
||||
}
|
||||
|
||||
// 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)
|
||||
default:
|
||||
req.Header[k] = vs
|
||||
}
|
||||
}
|
||||
|
||||
if d.EnableCompression {
|
||||
req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover")
|
||||
}
|
||||
|
||||
hostPort, hostNoPort := hostPortNoPort(u)
|
||||
|
||||
var proxyURL *url.URL
|
||||
// Check wether the proxy method has been configured
|
||||
if d.Proxy != nil {
|
||||
proxyURL, err = d.Proxy(req)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var targetHostPort string
|
||||
if proxyURL != nil {
|
||||
targetHostPort, _ = hostPortNoPort(proxyURL)
|
||||
} else {
|
||||
targetHostPort = hostPort
|
||||
}
|
||||
|
||||
var deadline time.Time
|
||||
if d.HandshakeTimeout != 0 {
|
||||
deadline = time.Now().Add(d.HandshakeTimeout)
|
||||
}
|
||||
|
||||
netDial := d.NetDial
|
||||
if netDial == nil {
|
||||
netDialer := &net.Dialer{Deadline: deadline}
|
||||
netDial = netDialer.Dial
|
||||
}
|
||||
|
||||
netConn, err := netDial("tcp", targetHostPort)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if netConn != nil {
|
||||
netConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := netConn.SetDeadline(deadline); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if proxyURL != nil {
|
||||
connectHeader := make(http.Header)
|
||||
if user := 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: "CONNECT",
|
||||
URL: &url.URL{Opaque: hostPort},
|
||||
Host: hostPort,
|
||||
Header: connectHeader,
|
||||
}
|
||||
|
||||
connectReq.Write(netConn)
|
||||
|
||||
// Read response.
|
||||
// Okay to use and discard buffered reader here, because
|
||||
// TLS server will not speak until spoken to.
|
||||
br := bufio.NewReader(netConn)
|
||||
resp, err := http.ReadResponse(br, connectReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
f := strings.SplitN(resp.Status, " ", 2)
|
||||
return nil, nil, errors.New(f[1])
|
||||
}
|
||||
}
|
||||
|
||||
if u.Scheme == "https" {
|
||||
cfg := cloneTLSConfig(d.TLSClientConfig)
|
||||
if cfg.ServerName == "" {
|
||||
cfg.ServerName = hostNoPort
|
||||
}
|
||||
tlsConn := tls.Client(netConn, cfg)
|
||||
netConn = tlsConn
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !cfg.InsecureSkipVerify {
|
||||
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize)
|
||||
|
||||
if err := req.Write(netConn); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(conn.br, req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if d.Jar != nil {
|
||||
if rc := resp.Cookies(); len(rc) > 0 {
|
||||
d.Jar.SetCookies(u, rc)
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode != 101 ||
|
||||
!strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") ||
|
||||
!strings.EqualFold(resp.Header.Get("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 = ioutil.NopCloser(bytes.NewReader(buf[:n]))
|
||||
return nil, resp, ErrBadHandshake
|
||||
}
|
||||
|
||||
for _, ext := range httpParseExtensions(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 = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
|
||||
|
||||
netConn.SetDeadline(time.Time{})
|
||||
netConn = nil // to avoid close in defer.
|
||||
return conn, resp, nil
|
||||
}
|
16
websocket/client_clone.go
Normal file
16
websocket/client_clone.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// 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.
|
||||
|
||||
// +build go1.8
|
||||
|
||||
package websocket
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
|
||||
if cfg == nil {
|
||||
return &tls.Config{}
|
||||
}
|
||||
return cfg.Clone()
|
||||
}
|
148
websocket/compression.go
Normal file
148
websocket/compression.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
// 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)
|
||||
fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil)
|
||||
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
|
||||
}
|
1149
websocket/conn.go
Normal file
1149
websocket/conn.go
Normal file
File diff suppressed because it is too large
Load Diff
18
websocket/conn_read.go
Normal file
18
websocket/conn_read.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.5
|
||||
|
||||
package websocket
|
||||
|
||||
import "io"
|
||||
|
||||
func (c *Conn) read(n int) ([]byte, error) {
|
||||
p, err := c.br.Peek(n)
|
||||
if err == io.EOF {
|
||||
err = errUnexpectedEOF
|
||||
}
|
||||
c.br.Discard(len(p))
|
||||
return p, err
|
||||
}
|
55
websocket/json.go
Normal file
55
websocket/json.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
// 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 is deprecated, use c.WriteJSON instead.
|
||||
func WriteJSON(c *Conn, v interface{}) error {
|
||||
return c.WriteJSON(v)
|
||||
}
|
||||
|
||||
// WriteJSON writes the JSON encoding of v to the connection.
|
||||
//
|
||||
// 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 is 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
|
||||
}
|
55
websocket/mask.go
Normal file
55
websocket/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.
|
||||
|
||||
// +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
|
||||
}
|
103
websocket/prepared.go
Normal file
103
websocket/prepared.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
// 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
|
||||
err error
|
||||
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 bool, 1)
|
||||
mu <- true
|
||||
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 }
|
289
websocket/server.go
Normal file
289
websocket/server.go
Normal file
|
@ -0,0 +1,289 @@
|
|||
// 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 (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type (
|
||||
OnUpgradeFunc func(*Conn, error)
|
||||
)
|
||||
|
||||
// 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.
|
||||
type Upgrader struct {
|
||||
// HandshakeTimeout specifies the duration for the handshake to complete.
|
||||
HandshakeTimeout time.Duration
|
||||
|
||||
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes. 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
|
||||
|
||||
// Subprotocols specifies the server's supported protocols in order of
|
||||
// preference. If this field is set, then the Upgrade method negotiates a
|
||||
// subprotocol by selecting the first match in this list with a protocol
|
||||
// requested by the client.
|
||||
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(ctx *fasthttp.RequestCtx, status int, reason error)
|
||||
|
||||
// CheckOrigin returns true if the request Origin header is acceptable. If
|
||||
// CheckOrigin is nil, the host in the Origin header must not be set or
|
||||
// must match the host of the request.
|
||||
CheckOrigin func(ctx *fasthttp.RequestCtx) 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(ctx *fasthttp.RequestCtx, status int, reason string) (*Conn, error) {
|
||||
err := HandshakeError{reason}
|
||||
if u.Error != nil {
|
||||
u.Error(ctx, status, err)
|
||||
} else {
|
||||
ctx.Response.Header.Set("Sec-Websocket-Version", "13")
|
||||
ctx.Error(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(ctx *fasthttp.RequestCtx) bool {
|
||||
origin := string(ctx.Request.Header.Peek("Origin"))
|
||||
if len(origin) == 0 {
|
||||
return true
|
||||
}
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return u.Host == string(ctx.Host())
|
||||
}
|
||||
|
||||
func (u *Upgrader) selectSubprotocol(ctx *fasthttp.RequestCtx, responseHeader *fasthttp.ResponseHeader) string {
|
||||
if u.Subprotocols != nil {
|
||||
clientProtocols := Subprotocols(ctx)
|
||||
for _, serverProtocol := range u.Subprotocols {
|
||||
for _, clientProtocol := range clientProtocols {
|
||||
if clientProtocol == serverProtocol {
|
||||
return clientProtocol
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if responseHeader != nil {
|
||||
return string(responseHeader.Peek("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) and the
|
||||
// application negotiated subprotocol (Sec-Websocket-Protocol).
|
||||
//
|
||||
// If the upgrade fails, then Upgrade replies to the client with an HTTP error
|
||||
// response.
|
||||
func (u *Upgrader) Upgrade(ctx *fasthttp.RequestCtx, responseHeader *fasthttp.ResponseHeader, cb OnUpgradeFunc) {
|
||||
if !ctx.IsGet() {
|
||||
cb(u.returnError(ctx, fasthttp.StatusMethodNotAllowed, "websocket: not a websocket handshake: request method is not GET"))
|
||||
return
|
||||
}
|
||||
|
||||
if v := responseHeader.Peek("Sec-Websocket-Extensions"); nil == v {
|
||||
cb(u.returnError(ctx, fasthttp.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported"))
|
||||
return
|
||||
}
|
||||
|
||||
if !tokenListContainsValue(&ctx.Request.Header, "Connection", "upgrade") {
|
||||
cb(u.returnError(ctx, fasthttp.StatusBadRequest, "websocket: not a websocket handshake: 'upgrade' token not found in 'Connection' header"))
|
||||
return
|
||||
}
|
||||
|
||||
if !tokenListContainsValue(&ctx.Request.Header, "Upgrade", "websocket") {
|
||||
cb(u.returnError(ctx, fasthttp.StatusBadRequest, "websocket: not a websocket handshake: 'websocket' token not found in 'Upgrade' header"))
|
||||
return
|
||||
}
|
||||
|
||||
if !tokenListContainsValue(&ctx.Request.Header, "Sec-Websocket-Version", "13") {
|
||||
cb(u.returnError(ctx, fasthttp.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header"))
|
||||
return
|
||||
}
|
||||
|
||||
checkOrigin := u.CheckOrigin
|
||||
if checkOrigin == nil {
|
||||
checkOrigin = checkSameOrigin
|
||||
}
|
||||
if !checkOrigin(ctx) {
|
||||
cb(u.returnError(ctx, fasthttp.StatusForbidden, "websocket: 'Origin' header value not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
challengeKey := string(ctx.Request.Header.Peek("Sec-Websocket-Key"))
|
||||
if challengeKey == "" {
|
||||
cb(u.returnError(ctx, fasthttp.StatusBadRequest, "websocket: not a websocket handshake: `Sec-Websocket-Key' header is missing or blank"))
|
||||
return
|
||||
}
|
||||
|
||||
subprotocol := u.selectSubprotocol(ctx, responseHeader)
|
||||
|
||||
// Negotiate PMCE
|
||||
var compress bool
|
||||
if u.EnableCompression {
|
||||
for _, ext := range parseExtensions(&ctx.Request.Header) {
|
||||
if ext[""] != "permessage-deflate" {
|
||||
continue
|
||||
}
|
||||
compress = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Hijack(func(netConn net.Conn) {
|
||||
var err error
|
||||
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize)
|
||||
c.subprotocol = subprotocol
|
||||
if compress {
|
||||
c.newCompressionWriter = compressNoContextTakeover
|
||||
c.newDecompressionReader = decompressNoContextTakeover
|
||||
}
|
||||
|
||||
p := c.writeBuf[: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"...)
|
||||
}
|
||||
|
||||
responseHeader.VisitAll(func(key, value []byte) {
|
||||
k := string(key)
|
||||
v := string(value)
|
||||
if k == "Sec-Websocket-Protocol" {
|
||||
return
|
||||
}
|
||||
|
||||
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"...)
|
||||
|
||||
// Clear deadlines set by HTTP server.
|
||||
netConn.SetDeadline(time.Time{})
|
||||
|
||||
if u.HandshakeTimeout > 0 {
|
||||
netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout))
|
||||
}
|
||||
if _, err = netConn.Write(p); err != nil {
|
||||
netConn.Close()
|
||||
cb(nil, err)
|
||||
return
|
||||
}
|
||||
if u.HandshakeTimeout > 0 {
|
||||
netConn.SetWriteDeadline(time.Time{})
|
||||
}
|
||||
|
||||
cb(c, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
|
||||
//
|
||||
// This function is deprecated, use websocket.Upgrader instead.
|
||||
//
|
||||
// The application is responsible for checking the request origin before
|
||||
// calling Upgrade. An example implementation of the same origin policy is:
|
||||
//
|
||||
// if req.Header.Get("Origin") != "http://"+req.Host {
|
||||
// http.Error(w, "Origin not allowed", 403)
|
||||
// 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(ctx *fasthttp.RequestCtx, responseHeader *fasthttp.ResponseHeader, readBufSize, writeBufSize int, cb OnUpgradeFunc) {
|
||||
u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize}
|
||||
u.Error = func(ctx *fasthttp.RequestCtx, status int, reason error) {
|
||||
// don't return errors to maintain backwards compatibility
|
||||
}
|
||||
u.CheckOrigin = func(ctx *fasthttp.RequestCtx) bool {
|
||||
// allow all connections by default
|
||||
return true
|
||||
}
|
||||
u.Upgrade(ctx, responseHeader, cb)
|
||||
}
|
||||
|
||||
// Subprotocols returns the subprotocols requested by the client in the
|
||||
// Sec-Websocket-Protocol header.
|
||||
func Subprotocols(ctx *fasthttp.RequestCtx) []string {
|
||||
h := strings.TrimSpace(string(ctx.Request.Header.Peek("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(ctx *fasthttp.RequestCtx) bool {
|
||||
return tokenListContainsValue(&ctx.Request.Header, "Connection", "upgrade") &&
|
||||
tokenListContainsValue(&ctx.Request.Header, "Upgrade", "websocket")
|
||||
}
|
272
websocket/util.go
Normal file
272
websocket/util.go
Normal file
|
@ -0,0 +1,272 @@
|
|||
// 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"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Octet types from RFC 2616.
|
||||
var octetTypes [256]byte
|
||||
|
||||
const (
|
||||
isTokenOctet = 1 << iota
|
||||
isSpaceOctet
|
||||
)
|
||||
|
||||
func init() {
|
||||
// From RFC 2616
|
||||
//
|
||||
// OCTET = <any 8-bit sequence of data>
|
||||
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||
// CR = <US-ASCII CR, carriage return (13)>
|
||||
// LF = <US-ASCII LF, linefeed (10)>
|
||||
// SP = <US-ASCII SP, space (32)>
|
||||
// HT = <US-ASCII HT, horizontal-tab (9)>
|
||||
// <"> = <US-ASCII double-quote mark (34)>
|
||||
// CRLF = CR LF
|
||||
// LWS = [CRLF] 1*( SP | HT )
|
||||
// TEXT = <any OCTET except CTLs, but including LWS>
|
||||
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
||||
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
||||
// token = 1*<any CHAR except CTLs or separators>
|
||||
// qdtext = <any TEXT except <">>
|
||||
|
||||
for c := 0; c < 256; c++ {
|
||||
var t byte
|
||||
isCtl := c <= 31 || c == 127
|
||||
isChar := 0 <= c && c <= 127
|
||||
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
|
||||
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
|
||||
t |= isSpaceOctet
|
||||
}
|
||||
if isChar && !isCtl && !isSeparator {
|
||||
t |= isTokenOctet
|
||||
}
|
||||
octetTypes[c] = t
|
||||
}
|
||||
}
|
||||
|
||||
func skipSpace(s string) (rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isSpaceOctet == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[i:]
|
||||
}
|
||||
|
||||
func nextToken(s string) (token string, rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isTokenOctet == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[:i], s[i:]
|
||||
}
|
||||
|
||||
func nextTokenOrQuoted(s string) (value string, rest string) {
|
||||
if !strings.HasPrefix(s, "\"") {
|
||||
return nextToken(s)
|
||||
}
|
||||
s = s[1:]
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '"':
|
||||
return s[:i], s[i+1:]
|
||||
case '\\':
|
||||
p := make([]byte, len(s)-1)
|
||||
j := copy(p, s[:i])
|
||||
escape := true
|
||||
for i = i + 1; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch {
|
||||
case escape:
|
||||
escape = false
|
||||
p[j] = b
|
||||
j += 1
|
||||
case b == '\\':
|
||||
escape = true
|
||||
case b == '"':
|
||||
return string(p[:j]), s[i+1:]
|
||||
default:
|
||||
p[j] = b
|
||||
j += 1
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// tokenListContainsValue returns true if the 1#token header with the given
|
||||
// name contains token.
|
||||
func tokenListContainsValue(header *fasthttp.RequestHeader, name string, value string) bool {
|
||||
s := string(header.Peek(name))
|
||||
for {
|
||||
var t string
|
||||
t, s = nextToken(skipSpace(s))
|
||||
if t == "" {
|
||||
break
|
||||
}
|
||||
s = skipSpace(s)
|
||||
if s != "" && s[0] != ',' {
|
||||
break
|
||||
}
|
||||
if strings.EqualFold(t, value) {
|
||||
return true
|
||||
}
|
||||
if s == "" {
|
||||
break
|
||||
}
|
||||
s = s[1:]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseExtensiosn parses WebSocket extensions from a header.
|
||||
func parseExtensions(header *fasthttp.RequestHeader) []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.
|
||||
s := string(header.Peek("Sec-Websocket-Extensions"))
|
||||
var result []map[string]string
|
||||
headers:
|
||||
for {
|
||||
var t string
|
||||
t, s = nextToken(skipSpace(s))
|
||||
if t == "" {
|
||||
break 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 == "" {
|
||||
break 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] != ';' {
|
||||
break headers
|
||||
}
|
||||
ext[k] = v
|
||||
}
|
||||
if s != "" && s[0] != ',' {
|
||||
break headers
|
||||
}
|
||||
result = append(result, ext)
|
||||
if s == "" {
|
||||
break headers
|
||||
}
|
||||
s = s[1:]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// parseExtensiosn parses WebSocket extensions from a header.
|
||||
func httpParseExtensions(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
|
||||
}
|
Loading…
Reference in New Issue
Block a user