diff --git a/go.mod b/go.mod index 618a87362c6..9b663fdd497 100644 --- a/go.mod +++ b/go.mod @@ -25,9 +25,9 @@ require ( github.com/golang/mock v1.4.4 github.com/google/gopacket v1.1.17 github.com/google/uuid v1.3.0 - github.com/googollee/go-socket.io v0.0.0-20181214084611-0ad7206c347a + github.com/googollee/go-socket.io v1.7.0 github.com/gorilla/mux v1.7.0 - github.com/gorilla/websocket v1.4.1 + github.com/gorilla/websocket v1.4.2 github.com/gosuri/uitable v0.0.0-20160404203958-36ee7e946282 github.com/hako/durafmt v0.0.0-20180520121703-7b7ae1e72ead github.com/hugozhu/godingtalk v1.0.6 @@ -165,12 +165,12 @@ require ( github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/gomodule/redigo v1.8.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/go-querystring v1.0.0 // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/googleapis/gax-go/v2 v2.0.5 // indirect github.com/googleapis/gnostic v0.4.1 // indirect - github.com/googollee/go-engine.io v0.0.0-20180829091931-e2f255711dcb // indirect github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect github.com/huandu/xstrings v1.2.0 // indirect diff --git a/go.sum b/go.sum index 39933c1b871..f0d360ddb13 100644 --- a/go.sum +++ b/go.sum @@ -293,6 +293,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.1.0+incompatible h1:sIa2eCvUTwgjbqXrPLfNwUf9S3i3mpH1O1atV+iL/Wk= github.com/gofrs/uuid v4.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -337,6 +338,8 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.8.4 h1:Z5JUg94HMTR1XpwBaSH4vq3+PNSIykBLxMdglbw10gg= +github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -384,17 +387,15 @@ github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/googollee/go-engine.io v0.0.0-20180829091931-e2f255711dcb h1:n22Aukg/TjoypWc37dbKIpCsz0VMFPD36HQk1WKvg3A= -github.com/googollee/go-engine.io v0.0.0-20180829091931-e2f255711dcb/go.mod h1:MBpz1MS3P4HtRcBpQU4HcjvWXZ9q+JWacMEh2/BFYbg= -github.com/googollee/go-socket.io v0.0.0-20181214084611-0ad7206c347a h1:NMY2a78Z98wdhMHJTJxTc9BFfnGGc5ZWfgY9a23zeck= -github.com/googollee/go-socket.io v0.0.0-20181214084611-0ad7206c347a/go.mod h1:ftBGBMhSYToR5oV4ImIPKvAIsNaTkLC+tTvoNafqxlQ= +github.com/googollee/go-socket.io v1.7.0 h1:ODcQSAvVIPvKozXtUGuJDV3pLwdpBLDs1Uoq/QHIlY8= +github.com/googollee/go-socket.io v1.7.0/go.mod h1:0vGP8/dXR9SZUMMD4+xxaGo/lohOw3YWMh2WRiWeKxg= github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd/go.mod h1:qkLSc0A5EXSP6B04TrN4oQoxqFI7A8XvoXSlJi8cwk8= github.com/gosuri/uiprogress v0.0.0-20170224063937-d0567a9d84a1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0= github.com/gosuri/uitable v0.0.0-20160404203958-36ee7e946282 h1:KFqmdzEPbU7Uck2tn50t+HQXZNVkxe8M9qRb/ZoSHaE= diff --git a/pkg/notify/service/handlers.go b/pkg/notify/service/handlers.go index 7e12bd5fbe9..dc4f7302732 100644 --- a/pkg/notify/service/handlers.go +++ b/pkg/notify/service/handlers.go @@ -21,6 +21,7 @@ import ( "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/notify/models" _ "yunion.io/x/onecloud/pkg/notify/sender" + "yunion.io/x/onecloud/pkg/notify/socket" ) const ( @@ -30,6 +31,7 @@ const ( func InitHandlers(app *appsrv.Application) { db.InitAllManagers() + socket.AddSocketHandlers("", app) models.InitEventLog() models.InitEmailQueue() diff --git a/pkg/notify/socket/clientmanager.go b/pkg/notify/socket/clientmanager.go new file mode 100644 index 00000000000..d2c22cfe46a --- /dev/null +++ b/pkg/notify/socket/clientmanager.go @@ -0,0 +1,111 @@ +package socket + +import ( + "context" + "fmt" + + socketio "github.com/googollee/go-socket.io" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" +) + +type Client struct { + Session string + Name string + UserID string + Conn socketio.Conn +} + +type ClientManager struct { + UserID2ClientMap map[string][]Client + SID2ClientMap map[string]Client + Server *socketio.Server + ForceSingleSessionLogin bool +} + +const ( + // BroadcastRoom 所有的在线 client 自动加入聊天室 + BroadcastRoom = "YunionBcast" +) + +// IsEmpty 当前是否无人在线(浏览器) +func (manager *ClientManager) IsEmpty() bool { + log.Debugf("[Clients] there are %d clients belonging to %d tenants.", len(manager.SID2ClientMap), len(manager.UserID2ClientMap)) + return len(manager.SID2ClientMap)+len(manager.UserID2ClientMap) == 0 +} + +// Register 浏览器user 登录时,注册 socketio 链接 +func (manager *ClientManager) Register(ctx context.Context, s socketio.Conn) error { + session, cred, err := getCred(ctx, s) + if err != nil { + log.Errorf("login FAILED: with id %s error: %v", s.ID(), err) + return errors.Wrapf(err, "getCred") + } + log.Debugf("Login PASS ! ID: %s , user: %s(%s)", s.ID(), cred.GetUserName(), cred.GetUserId()) + UserID := cred.GetUserId() + client := Client{ + Session: session, + Name: cred.GetUserName(), + UserID: UserID, + Conn: s, + } + + manager.UserID2ClientMap[UserID] = append(manager.UserID2ClientMap[UserID], client) + manager.SID2ClientMap[s.ID()] = client + log.Debugf("registered successful for user %s(%s) with id %s", cred.GetUserName(), cred.GetUserId(), s.ID()) + s.Join(BroadcastRoom) + s.SetContext("") + return nil +} + +// Gretting 组合一个显示用户 socketio 链接信息的子串 +func (manager *ClientManager) Gretting(s socketio.Conn) string { + client := manager.SID2ClientMap[s.ID()] + return fmt.Sprintf("hello %s(%s), your socket io id: %s, your session: %s.", client.Name, client.UserID, s.ID(), client.Session) +} + +// Unregister 浏览器 user 刷新或关闭页面,断开链接(自动重连) +func (manager *ClientManager) Unregister(s socketio.Conn, reason string) { + delete(manager.SID2ClientMap, s.ID()) + log.Debugf("[ Unregister ] ID: %s; reason: %s", s.ID(), reason) + s.Emit("bye", "") + s.Leave(BroadcastRoom) + s.Close() +} + +// NotifyByUserID 按照用户 id,通知到用户的所有在线浏览器页面。支持用户多 session 登录,例如 sysadmin +func (manager *ClientManager) NotifyByUserID(message string, UserID string) error { + count := 0 + name := "" + msg := "" + + for _, c := range manager.UserID2ClientMap[UserID] { + if c.UserID == UserID { + c.Conn.Emit("message", string(message)) + name = manager.SID2ClientMap[c.Conn.ID()].Name + count++ + } + } + + if count == 0 { + log.Warningf("UserID %s is not online!", UserID) + return errors.Errorf(msg) + } + log.Debugf("[%d clients] NOTIFY OK to %s(@%s) ", count, UserID, name) + return nil +} + +//Broadcasts 对所有在线用户发广播 +func (manager *ClientManager) Broadcasts(message string) { + // BroadcastToRoom(namespace string, room, event string, args ...interface{}) bool { + if manager.IsEmpty() { + log.Debugf("Ignore Broadcasting for empty room\n") + return + } + if !manager.Server.BroadcastToRoom("", BroadcastRoom, "message", message) { + log.Errorf("broadcasting error with msg %s\n", message) + return + } + log.Infof("broadcasting OK with msg %s\n", message) +} diff --git a/pkg/notify/socket/doc.go b/pkg/notify/socket/doc.go new file mode 100644 index 00000000000..be62f2b9345 --- /dev/null +++ b/pkg/notify/socket/doc.go @@ -0,0 +1,15 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package socket // import "yunion.io/x/onecloud/pkg/notify/tasks" diff --git a/pkg/notify/socket/login.go b/pkg/notify/socket/login.go new file mode 100644 index 00000000000..5faff059f95 --- /dev/null +++ b/pkg/notify/socket/login.go @@ -0,0 +1,39 @@ +package socket + +import ( + "context" + + socketio "github.com/googollee/go-socket.io" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/apigateway/clientman" + "yunion.io/x/onecloud/pkg/mcclient" +) + +// getCred 与前端约定,在 socketio 里,使用 ?session=XXX 的方式获取用户的 session +func getCred(ctx context.Context, s socketio.Conn) (string, mcclient.TokenCredential, error) { + query, err := jsonutils.ParseQueryString(s.URL().RawQuery) + if err != nil { + return "", nil, errors.Wrapf(err, "ParseQueryString") + } + session, err := query.GetString("session") + if err != nil { + return "", nil, errors.Wrapf(err, "get session") + } + if len(session) == 0 { + return "", nil, errors.Errorf("empty session") + } + // tm := clientman.NewMapTokenManagerV2() 这行注释临时保留 -rex. + // cred := clientman.TokenMan.Get(session) + authToken, err := clientman.Decode(session) + if err != nil { + return "", nil, errors.Wrap(err, "Decode") + } + cred, err := authToken.GetToken(ctx) + if err != nil { + return "", nil, errors.Wrap(err, "authToken.GetToken") + } + return session, cred, nil +} diff --git a/pkg/notify/socket/msg.go b/pkg/notify/socket/msg.go new file mode 100644 index 00000000000..8abb819a2b0 --- /dev/null +++ b/pkg/notify/socket/msg.go @@ -0,0 +1,21 @@ +package socket + +type SMsgEntry struct { + ObjType string `json:"obj_type"` + ObjId string `json:"obj_id"` + ObjName string `json:"obj_name"` + Success bool `json:"success"` + Action string `json:"action"` + Notes string `json:"notes"` + UserId string `json:"user_id"` + User string `json:"user"` + TenantId string `json:"tenant_id"` + Tenant string `json:"tenant"` + Broadcast bool `json:"broadcast"` + //控制前端是否进行弹窗信息提示 + IgnoreAlert bool `json:"ignore_alert"` +} + +type SMsg struct { + WebSocket SMsgEntry `json:"notification"` +} diff --git a/pkg/notify/socket/socket.go b/pkg/notify/socket/socket.go new file mode 100644 index 00000000000..6b9104da535 --- /dev/null +++ b/pkg/notify/socket/socket.go @@ -0,0 +1,149 @@ +package socket + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "time" + + socketio "github.com/googollee/go-socket.io" + engineio "github.com/googollee/go-socket.io/engineio" + "github.com/googollee/go-socket.io/engineio/transport" + "github.com/googollee/go-socket.io/engineio/transport/polling" + "github.com/googollee/go-socket.io/engineio/transport/websocket" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/appsrv" + "yunion.io/x/onecloud/pkg/mcclient/auth" +) + +type Message struct { + Sender string `json:"sender,omitempty"` + Recipient string `json:"recipient,omitempty"` + Content string `json:"content,omitempty"` +} + +var ( + SocketServer *socketio.Server + DefaultSocketMan = ClientManager{ + UserID2ClientMap: make(map[string][]Client), + SID2ClientMap: make(map[string]Client), + Server: nil, + ForceSingleSessionLogin: false, + } +) + +func init() { + SocketServer = NewSocketServer() + if SocketServer == nil { + log.Fatalln("got nil socketio server !") + os.Exit(1) + } + DefaultSocketMan.Server = SocketServer +} + +func NewSocketServer() *socketio.Server { + + pt := polling.Default + + wt := websocket.Default + wt.CheckOrigin = func(req *http.Request) bool { + return true + } + server := socketio.NewServer(&engineio.Options{ + Transports: []transport.Transport{ + pt, + wt, + }, + }) + + server.OnConnect("/", func(s socketio.Conn) error { + ctx := context.WithValue(context.Background(), "", "") + err := DefaultSocketMan.Register(ctx, s) + if err != nil { + return errors.Wrapf(err, "Register") + } + s.SetContext("") + return nil + }) + + server.OnEvent("/", "bye", func(s socketio.Conn) string { + last := s.Context().(string) + DefaultSocketMan.Unregister(s, "bye") + return last + }) + + server.OnError("/", func(s socketio.Conn, err error) { + log.Errorf("meet error: %v", err) + }) + + server.OnDisconnect("/", func(s socketio.Conn, reason string) { + DefaultSocketMan.Unregister(s, reason) + }) + + return server +} + +// SocketHandler 将 http 的上下文传递给 socketio +func SocketHandler(ctx context.Context, w http.ResponseWriter, req *http.Request) { + req = req.Clone(ctx) + SocketServer.ServeHTTP(w, req) +} + +func SocketMiddleware(f func(context.Context, http.ResponseWriter, *http.Request)) func(context.Context, http.ResponseWriter, *http.Request) { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + f(ctx, w, r) + } +} + +/* +新建websocket handle。默认1周超时,100个worker. +*/ +func NewSocketIOHandler(method, prefix string, handler func(context.Context, http.ResponseWriter, *http.Request)) *appsrv.SHandlerInfo { + log.Debugf("%s - %s", method, prefix) + hi := appsrv.SHandlerInfo{} + hi.SetMethod(method) + hi.SetPath(prefix) + hi.SetHandler(handler) + hi.SetProcessTimeout(12 * time.Hour) // session timeout + hi.SetWorkerManager(GetSocketWorker()) + return &hi +} + +// AddSocketHandlers 路径分发 +func AddSocketHandlers(prefix string, app *appsrv.Application) { + getSocketIO := NewSocketIOHandler("GET", fmt.Sprintf("%s/socket.io/", prefix), SocketMiddleware(SocketHandler)) + postSocketIO := NewSocketIOHandler("POST", fmt.Sprintf("%s/socket.io/", prefix), SocketMiddleware(SocketHandler)) + postWebSocket := NewSocketIOHandler("POST", fmt.Sprintf("%s/websockets", prefix), auth.Authenticate(notification)) + app.AddHandler3(getSocketIO) + app.AddHandler3(postSocketIO) + app.AddHandler3(postWebSocket) +} + +// notification 外部(http)通知接口,用于向单个用户或全部用户发消息 +func notification(ctx context.Context, w http.ResponseWriter, r *http.Request) { + var body SMsg + + err := json.NewDecoder(r.Body).Decode(&body) + if err != nil { + log.Fatalf("[notification] decode body [%v] error: %s", r.Body, err) + return + } + message, err := json.Marshal(body.WebSocket) + if err != nil { + log.Fatalf("Marshal body error: %s", err) + return + } + + if body.WebSocket.Broadcast { + DefaultSocketMan.Broadcasts(string(message)) + log.Infof("message: %+v", message) + } else { + DefaultSocketMan.NotifyByUserID(string(message), body.WebSocket.UserId) + log.Errorf("message: %+v", message) + } +} diff --git a/pkg/notify/socket/worker.go b/pkg/notify/socket/worker.go new file mode 100644 index 00000000000..8bca92037c3 --- /dev/null +++ b/pkg/notify/socket/worker.go @@ -0,0 +1,14 @@ +package socket + +import "yunion.io/x/onecloud/pkg/appsrv" + +var socketWorker *appsrv.SWorkerManager + +func GetSocketWorker() *appsrv.SWorkerManager { + if socketWorker == nil { + // allow 1024 user online + socketWorker = appsrv.NewWorkerManager("ws", 1024, 1, false) + } + + return socketWorker +} diff --git a/vendor/github.com/gomodule/redigo/LICENSE b/vendor/github.com/gomodule/redigo/LICENSE new file mode 100644 index 00000000000..f433b1a53f5 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/vendor/github.com/gomodule/redigo/redis/commandinfo.go b/vendor/github.com/gomodule/redigo/redis/commandinfo.go new file mode 100644 index 00000000000..b6df6a25aa3 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/commandinfo.go @@ -0,0 +1,55 @@ +// Copyright 2014 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "strings" +) + +const ( + connectionWatchState = 1 << iota + connectionMultiState + connectionSubscribeState + connectionMonitorState +) + +type commandInfo struct { + // Set or Clear these states on connection. + Set, Clear int +} + +var commandInfos = map[string]commandInfo{ + "WATCH": {Set: connectionWatchState}, + "UNWATCH": {Clear: connectionWatchState}, + "MULTI": {Set: connectionMultiState}, + "EXEC": {Clear: connectionWatchState | connectionMultiState}, + "DISCARD": {Clear: connectionWatchState | connectionMultiState}, + "PSUBSCRIBE": {Set: connectionSubscribeState}, + "SUBSCRIBE": {Set: connectionSubscribeState}, + "MONITOR": {Set: connectionMonitorState}, +} + +func init() { + for n, ci := range commandInfos { + commandInfos[strings.ToLower(n)] = ci + } +} + +func lookupCommandInfo(commandName string) commandInfo { + if ci, ok := commandInfos[commandName]; ok { + return ci + } + return commandInfos[strings.ToUpper(commandName)] +} diff --git a/vendor/github.com/gomodule/redigo/redis/conn.go b/vendor/github.com/gomodule/redigo/redis/conn.go new file mode 100644 index 00000000000..1398b4d1d22 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/conn.go @@ -0,0 +1,787 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/url" + "regexp" + "strconv" + "sync" + "time" +) + +var ( + _ ConnWithTimeout = (*conn)(nil) +) + +// conn is the low-level implementation of Conn +type conn struct { + // Shared + mu sync.Mutex + pending int + err error + conn net.Conn + + // Read + readTimeout time.Duration + br *bufio.Reader + + // Write + writeTimeout time.Duration + bw *bufio.Writer + + // Scratch space for formatting argument length. + // '*' or '$', length, "\r\n" + lenScratch [32]byte + + // Scratch space for formatting integers and floats. + numScratch [40]byte +} + +// DialTimeout acts like Dial but takes timeouts for establishing the +// connection to the server, writing a command and reading a reply. +// +// Deprecated: Use Dial with options instead. +func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) { + return Dial(network, address, + DialConnectTimeout(connectTimeout), + DialReadTimeout(readTimeout), + DialWriteTimeout(writeTimeout)) +} + +// DialOption specifies an option for dialing a Redis server. +type DialOption struct { + f func(*dialOptions) +} + +type dialOptions struct { + readTimeout time.Duration + writeTimeout time.Duration + tlsHandshakeTimeout time.Duration + dialer *net.Dialer + dialContext func(ctx context.Context, network, addr string) (net.Conn, error) + db int + username string + password string + clientName string + useTLS bool + skipVerify bool + tlsConfig *tls.Config +} + +// DialTLSHandshakeTimeout specifies the maximum amount of time waiting to +// wait for a TLS handshake. Zero means no timeout. +// If no DialTLSHandshakeTimeout option is specified then the default is 30 seconds. +func DialTLSHandshakeTimeout(d time.Duration) DialOption { + return DialOption{func(do *dialOptions) { + do.tlsHandshakeTimeout = d + }} +} + +// DialReadTimeout specifies the timeout for reading a single command reply. +func DialReadTimeout(d time.Duration) DialOption { + return DialOption{func(do *dialOptions) { + do.readTimeout = d + }} +} + +// DialWriteTimeout specifies the timeout for writing a single command. +func DialWriteTimeout(d time.Duration) DialOption { + return DialOption{func(do *dialOptions) { + do.writeTimeout = d + }} +} + +// DialConnectTimeout specifies the timeout for connecting to the Redis server when +// no DialNetDial option is specified. +// If no DialConnectTimeout option is specified then the default is 30 seconds. +func DialConnectTimeout(d time.Duration) DialOption { + return DialOption{func(do *dialOptions) { + do.dialer.Timeout = d + }} +} + +// DialKeepAlive specifies the keep-alive period for TCP connections to the Redis server +// when no DialNetDial option is specified. +// If zero, keep-alives are not enabled. If no DialKeepAlive option is specified then +// the default of 5 minutes is used to ensure that half-closed TCP sessions are detected. +func DialKeepAlive(d time.Duration) DialOption { + return DialOption{func(do *dialOptions) { + do.dialer.KeepAlive = d + }} +} + +// DialNetDial specifies a custom dial function for creating TCP +// connections, otherwise a net.Dialer customized via the other options is used. +// DialNetDial overrides DialConnectTimeout and DialKeepAlive. +func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption { + return DialOption{func(do *dialOptions) { + do.dialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return dial(network, addr) + } + }} +} + +// DialContextFunc specifies a custom dial function with context for creating TCP +// connections, otherwise a net.Dialer customized via the other options is used. +// DialContextFunc overrides DialConnectTimeout and DialKeepAlive. +func DialContextFunc(f func(ctx context.Context, network, addr string) (net.Conn, error)) DialOption { + return DialOption{func(do *dialOptions) { + do.dialContext = f + }} +} + +// DialDatabase specifies the database to select when dialing a connection. +func DialDatabase(db int) DialOption { + return DialOption{func(do *dialOptions) { + do.db = db + }} +} + +// DialPassword specifies the password to use when connecting to +// the Redis server. +func DialPassword(password string) DialOption { + return DialOption{func(do *dialOptions) { + do.password = password + }} +} + +// DialUsername specifies the username to use when connecting to +// the Redis server when Redis ACLs are used. +func DialUsername(username string) DialOption { + return DialOption{func(do *dialOptions) { + do.username = username + }} +} + +// DialClientName specifies a client name to be used +// by the Redis server connection. +func DialClientName(name string) DialOption { + return DialOption{func(do *dialOptions) { + do.clientName = name + }} +} + +// DialTLSConfig specifies the config to use when a TLS connection is dialed. +// Has no effect when not dialing a TLS connection. +func DialTLSConfig(c *tls.Config) DialOption { + return DialOption{func(do *dialOptions) { + do.tlsConfig = c + }} +} + +// DialTLSSkipVerify disables server name verification when connecting over +// TLS. Has no effect when not dialing a TLS connection. +func DialTLSSkipVerify(skip bool) DialOption { + return DialOption{func(do *dialOptions) { + do.skipVerify = skip + }} +} + +// DialUseTLS specifies whether TLS should be used when connecting to the +// server. This option is ignore by DialURL. +func DialUseTLS(useTLS bool) DialOption { + return DialOption{func(do *dialOptions) { + do.useTLS = useTLS + }} +} + +// Dial connects to the Redis server at the given network and +// address using the specified options. +func Dial(network, address string, options ...DialOption) (Conn, error) { + return DialContext(context.Background(), network, address, options...) +} + +type tlsHandshakeTimeoutError struct{} + +func (tlsHandshakeTimeoutError) Timeout() bool { return true } +func (tlsHandshakeTimeoutError) Temporary() bool { return true } +func (tlsHandshakeTimeoutError) Error() string { return "TLS handshake timeout" } + +// DialContext connects to the Redis server at the given network and +// address using the specified options and context. +func DialContext(ctx context.Context, network, address string, options ...DialOption) (Conn, error) { + do := dialOptions{ + dialer: &net.Dialer{ + Timeout: time.Second * 30, + KeepAlive: time.Minute * 5, + }, + tlsHandshakeTimeout: time.Second * 10, + } + for _, option := range options { + option.f(&do) + } + if do.dialContext == nil { + do.dialContext = do.dialer.DialContext + } + + netConn, err := do.dialContext(ctx, network, address) + if err != nil { + return nil, err + } + + if do.useTLS { + var tlsConfig *tls.Config + if do.tlsConfig == nil { + tlsConfig = &tls.Config{InsecureSkipVerify: do.skipVerify} + } else { + tlsConfig = cloneTLSConfig(do.tlsConfig) + } + if tlsConfig.ServerName == "" { + host, _, err := net.SplitHostPort(address) + if err != nil { + netConn.Close() + return nil, err + } + tlsConfig.ServerName = host + } + + tlsConn := tls.Client(netConn, tlsConfig) + errc := make(chan error, 2) // buffered so we don't block timeout or Handshake + if d := do.tlsHandshakeTimeout; d != 0 { + timer := time.AfterFunc(d, func() { + errc <- tlsHandshakeTimeoutError{} + }) + defer timer.Stop() + } + go func() { + errc <- tlsConn.Handshake() + }() + if err := <-errc; err != nil { + // Timeout or Handshake error. + netConn.Close() // nolint: errcheck + return nil, err + } + + netConn = tlsConn + } + + c := &conn{ + conn: netConn, + bw: bufio.NewWriter(netConn), + br: bufio.NewReader(netConn), + readTimeout: do.readTimeout, + writeTimeout: do.writeTimeout, + } + + if do.password != "" { + authArgs := make([]interface{}, 0, 2) + if do.username != "" { + authArgs = append(authArgs, do.username) + } + authArgs = append(authArgs, do.password) + if _, err := c.Do("AUTH", authArgs...); err != nil { + netConn.Close() + return nil, err + } + } + + if do.clientName != "" { + if _, err := c.Do("CLIENT", "SETNAME", do.clientName); err != nil { + netConn.Close() + return nil, err + } + } + + if do.db != 0 { + if _, err := c.Do("SELECT", do.db); err != nil { + netConn.Close() + return nil, err + } + } + + return c, nil +} + +var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`) + +// DialURL connects to a Redis server at the given URL using the Redis +// URI scheme. URLs should follow the draft IANA specification for the +// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis). +func DialURL(rawurl string, options ...DialOption) (Conn, error) { + u, err := url.Parse(rawurl) + if err != nil { + return nil, err + } + + if u.Scheme != "redis" && u.Scheme != "rediss" { + return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme) + } + + if u.Opaque != "" { + return nil, fmt.Errorf("invalid redis URL, url is opaque: %s", rawurl) + } + + // As per the IANA draft spec, the host defaults to localhost and + // the port defaults to 6379. + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + // assume port is missing + host = u.Host + port = "6379" + } + if host == "" { + host = "localhost" + } + address := net.JoinHostPort(host, port) + + if u.User != nil { + password, isSet := u.User.Password() + if isSet { + options = append(options, DialUsername(u.User.Username()), DialPassword(password)) + } + } + + match := pathDBRegexp.FindStringSubmatch(u.Path) + if len(match) == 2 { + db := 0 + if len(match[1]) > 0 { + db, err = strconv.Atoi(match[1]) + if err != nil { + return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) + } + } + if db != 0 { + options = append(options, DialDatabase(db)) + } + } else if u.Path != "" { + return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) + } + + options = append(options, DialUseTLS(u.Scheme == "rediss")) + + return Dial("tcp", address, options...) +} + +// NewConn returns a new Redigo connection for the given net connection. +func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn { + return &conn{ + conn: netConn, + bw: bufio.NewWriter(netConn), + br: bufio.NewReader(netConn), + readTimeout: readTimeout, + writeTimeout: writeTimeout, + } +} + +func (c *conn) Close() error { + c.mu.Lock() + err := c.err + if c.err == nil { + c.err = errors.New("redigo: closed") + err = c.conn.Close() + } + c.mu.Unlock() + return err +} + +func (c *conn) fatal(err error) error { + c.mu.Lock() + if c.err == nil { + c.err = err + // Close connection to force errors on subsequent calls and to unblock + // other reader or writer. + c.conn.Close() + } + c.mu.Unlock() + return err +} + +func (c *conn) Err() error { + c.mu.Lock() + err := c.err + c.mu.Unlock() + return err +} + +func (c *conn) writeLen(prefix byte, n int) error { + c.lenScratch[len(c.lenScratch)-1] = '\n' + c.lenScratch[len(c.lenScratch)-2] = '\r' + i := len(c.lenScratch) - 3 + for { + c.lenScratch[i] = byte('0' + n%10) + i -= 1 + n = n / 10 + if n == 0 { + break + } + } + c.lenScratch[i] = prefix + _, err := c.bw.Write(c.lenScratch[i:]) + return err +} + +func (c *conn) writeString(s string) error { + if err := c.writeLen('$', len(s)); err != nil { + return err + } + if _, err := c.bw.WriteString(s); err != nil { + return err + } + _, err := c.bw.WriteString("\r\n") + return err +} + +func (c *conn) writeBytes(p []byte) error { + if err := c.writeLen('$', len(p)); err != nil { + return err + } + if _, err := c.bw.Write(p); err != nil { + return err + } + _, err := c.bw.WriteString("\r\n") + return err +} + +func (c *conn) writeInt64(n int64) error { + return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10)) +} + +func (c *conn) writeFloat64(n float64) error { + return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64)) +} + +func (c *conn) writeCommand(cmd string, args []interface{}) error { + if err := c.writeLen('*', 1+len(args)); err != nil { + return err + } + if err := c.writeString(cmd); err != nil { + return err + } + for _, arg := range args { + if err := c.writeArg(arg, true); err != nil { + return err + } + } + return nil +} + +func (c *conn) writeArg(arg interface{}, argumentTypeOK bool) (err error) { + switch arg := arg.(type) { + case string: + return c.writeString(arg) + case []byte: + return c.writeBytes(arg) + case int: + return c.writeInt64(int64(arg)) + case int64: + return c.writeInt64(arg) + case float64: + return c.writeFloat64(arg) + case bool: + if arg { + return c.writeString("1") + } else { + return c.writeString("0") + } + case nil: + return c.writeString("") + case Argument: + if argumentTypeOK { + return c.writeArg(arg.RedisArg(), false) + } + // See comment in default clause below. + var buf bytes.Buffer + fmt.Fprint(&buf, arg) + return c.writeBytes(buf.Bytes()) + default: + // This default clause is intended to handle builtin numeric types. + // The function should return an error for other types, but this is not + // done for compatibility with previous versions of the package. + var buf bytes.Buffer + fmt.Fprint(&buf, arg) + return c.writeBytes(buf.Bytes()) + } +} + +type protocolError string + +func (pe protocolError) Error() string { + return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe)) +} + +// readLine reads a line of input from the RESP stream. +func (c *conn) readLine() ([]byte, error) { + // To avoid allocations, attempt to read the line using ReadSlice. This + // call typically succeeds. The known case where the call fails is when + // reading the output from the MONITOR command. + p, err := c.br.ReadSlice('\n') + if err == bufio.ErrBufferFull { + // The line does not fit in the bufio.Reader's buffer. Fall back to + // allocating a buffer for the line. + buf := append([]byte{}, p...) + for err == bufio.ErrBufferFull { + p, err = c.br.ReadSlice('\n') + buf = append(buf, p...) + } + p = buf + } + if err != nil { + return nil, err + } + i := len(p) - 2 + if i < 0 || p[i] != '\r' { + return nil, protocolError("bad response line terminator") + } + return p[:i], nil +} + +// parseLen parses bulk string and array lengths. +func parseLen(p []byte) (int, error) { + if len(p) == 0 { + return -1, protocolError("malformed length") + } + + if p[0] == '-' && len(p) == 2 && p[1] == '1' { + // handle $-1 and $-1 null replies. + return -1, nil + } + + var n int + for _, b := range p { + n *= 10 + if b < '0' || b > '9' { + return -1, protocolError("illegal bytes in length") + } + n += int(b - '0') + } + + return n, nil +} + +// parseInt parses an integer reply. +func parseInt(p []byte) (interface{}, error) { + if len(p) == 0 { + return 0, protocolError("malformed integer") + } + + var negate bool + if p[0] == '-' { + negate = true + p = p[1:] + if len(p) == 0 { + return 0, protocolError("malformed integer") + } + } + + var n int64 + for _, b := range p { + n *= 10 + if b < '0' || b > '9' { + return 0, protocolError("illegal bytes in length") + } + n += int64(b - '0') + } + + if negate { + n = -n + } + return n, nil +} + +var ( + okReply interface{} = "OK" + pongReply interface{} = "PONG" +) + +func (c *conn) readReply() (interface{}, error) { + line, err := c.readLine() + if err != nil { + return nil, err + } + if len(line) == 0 { + return nil, protocolError("short response line") + } + switch line[0] { + case '+': + switch string(line[1:]) { + case "OK": + // Avoid allocation for frequent "+OK" response. + return okReply, nil + case "PONG": + // Avoid allocation in PING command benchmarks :) + return pongReply, nil + default: + return string(line[1:]), nil + } + case '-': + return Error(line[1:]), nil + case ':': + return parseInt(line[1:]) + case '$': + n, err := parseLen(line[1:]) + if n < 0 || err != nil { + return nil, err + } + p := make([]byte, n) + _, err = io.ReadFull(c.br, p) + if err != nil { + return nil, err + } + if line, err := c.readLine(); err != nil { + return nil, err + } else if len(line) != 0 { + return nil, protocolError("bad bulk string format") + } + return p, nil + case '*': + n, err := parseLen(line[1:]) + if n < 0 || err != nil { + return nil, err + } + r := make([]interface{}, n) + for i := range r { + r[i], err = c.readReply() + if err != nil { + return nil, err + } + } + return r, nil + } + return nil, protocolError("unexpected response line") +} + +func (c *conn) Send(cmd string, args ...interface{}) error { + c.mu.Lock() + c.pending += 1 + c.mu.Unlock() + if c.writeTimeout != 0 { + if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)); err != nil { + return c.fatal(err) + } + } + if err := c.writeCommand(cmd, args); err != nil { + return c.fatal(err) + } + return nil +} + +func (c *conn) Flush() error { + if c.writeTimeout != 0 { + if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)); err != nil { + return c.fatal(err) + } + } + if err := c.bw.Flush(); err != nil { + return c.fatal(err) + } + return nil +} + +func (c *conn) Receive() (interface{}, error) { + return c.ReceiveWithTimeout(c.readTimeout) +} + +func (c *conn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) { + var deadline time.Time + if timeout != 0 { + deadline = time.Now().Add(timeout) + } + if err := c.conn.SetReadDeadline(deadline); err != nil { + return nil, c.fatal(err) + } + + if reply, err = c.readReply(); err != nil { + return nil, c.fatal(err) + } + // When using pub/sub, the number of receives can be greater than the + // number of sends. To enable normal use of the connection after + // unsubscribing from all channels, we do not decrement pending to a + // negative value. + // + // The pending field is decremented after the reply is read to handle the + // case where Receive is called before Send. + c.mu.Lock() + if c.pending > 0 { + c.pending -= 1 + } + c.mu.Unlock() + if err, ok := reply.(Error); ok { + return nil, err + } + return +} + +func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) { + return c.DoWithTimeout(c.readTimeout, cmd, args...) +} + +func (c *conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error) { + c.mu.Lock() + pending := c.pending + c.pending = 0 + c.mu.Unlock() + + if cmd == "" && pending == 0 { + return nil, nil + } + + if c.writeTimeout != 0 { + if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)); err != nil { + return nil, c.fatal(err) + } + } + + if cmd != "" { + if err := c.writeCommand(cmd, args); err != nil { + return nil, c.fatal(err) + } + } + + if err := c.bw.Flush(); err != nil { + return nil, c.fatal(err) + } + + var deadline time.Time + if readTimeout != 0 { + deadline = time.Now().Add(readTimeout) + } + if err := c.conn.SetReadDeadline(deadline); err != nil { + return nil, c.fatal(err) + } + + if cmd == "" { + reply := make([]interface{}, pending) + for i := range reply { + r, e := c.readReply() + if e != nil { + return nil, c.fatal(e) + } + reply[i] = r + } + return reply, nil + } + + var err error + var reply interface{} + for i := 0; i <= pending; i++ { + var e error + if reply, e = c.readReply(); e != nil { + return nil, c.fatal(e) + } + if e, ok := reply.(Error); ok && err == nil { + err = e + } + } + return reply, err +} diff --git a/vendor/github.com/gomodule/redigo/redis/doc.go b/vendor/github.com/gomodule/redigo/redis/doc.go new file mode 100644 index 00000000000..69ad506cd3a --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/doc.go @@ -0,0 +1,177 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +// Package redis is a client for the Redis database. +// +// The Redigo FAQ (https://github.com/gomodule/redigo/wiki/FAQ) contains more +// documentation about this package. +// +// Connections +// +// The Conn interface is the primary interface for working with Redis. +// Applications create connections by calling the Dial, DialWithTimeout or +// NewConn functions. In the future, functions will be added for creating +// sharded and other types of connections. +// +// The application must call the connection Close method when the application +// is done with the connection. +// +// Executing Commands +// +// The Conn interface has a generic method for executing Redis commands: +// +// Do(commandName string, args ...interface{}) (reply interface{}, err error) +// +// The Redis command reference (http://redis.io/commands) lists the available +// commands. An example of using the Redis APPEND command is: +// +// n, err := conn.Do("APPEND", "key", "value") +// +// The Do method converts command arguments to bulk strings for transmission +// to the server as follows: +// +// Go Type Conversion +// []byte Sent as is +// string Sent as is +// int, int64 strconv.FormatInt(v) +// float64 strconv.FormatFloat(v, 'g', -1, 64) +// bool true -> "1", false -> "0" +// nil "" +// all other types fmt.Fprint(w, v) +// +// Redis command reply types are represented using the following Go types: +// +// Redis type Go type +// error redis.Error +// integer int64 +// simple string string +// bulk string []byte or nil if value not present. +// array []interface{} or nil if value not present. +// +// Use type assertions or the reply helper functions to convert from +// interface{} to the specific Go type for the command result. +// +// Pipelining +// +// Connections support pipelining using the Send, Flush and Receive methods. +// +// Send(commandName string, args ...interface{}) error +// Flush() error +// Receive() (reply interface{}, err error) +// +// Send writes the command to the connection's output buffer. Flush flushes the +// connection's output buffer to the server. Receive reads a single reply from +// the server. The following example shows a simple pipeline. +// +// c.Send("SET", "foo", "bar") +// c.Send("GET", "foo") +// c.Flush() +// c.Receive() // reply from SET +// v, err = c.Receive() // reply from GET +// +// The Do method combines the functionality of the Send, Flush and Receive +// methods. The Do method starts by writing the command and flushing the output +// buffer. Next, the Do method receives all pending replies including the reply +// for the command just sent by Do. If any of the received replies is an error, +// then Do returns the error. If there are no errors, then Do returns the last +// reply. If the command argument to the Do method is "", then the Do method +// will flush the output buffer and receive pending replies without sending a +// command. +// +// Use the Send and Do methods to implement pipelined transactions. +// +// c.Send("MULTI") +// c.Send("INCR", "foo") +// c.Send("INCR", "bar") +// r, err := c.Do("EXEC") +// fmt.Println(r) // prints [1, 1] +// +// Concurrency +// +// Connections support one concurrent caller to the Receive method and one +// concurrent caller to the Send and Flush methods. No other concurrency is +// supported including concurrent calls to the Do and Close methods. +// +// For full concurrent access to Redis, use the thread-safe Pool to get, use +// and release a connection from within a goroutine. Connections returned from +// a Pool have the concurrency restrictions described in the previous +// paragraph. +// +// Publish and Subscribe +// +// Use the Send, Flush and Receive methods to implement Pub/Sub subscribers. +// +// c.Send("SUBSCRIBE", "example") +// c.Flush() +// for { +// reply, err := c.Receive() +// if err != nil { +// return err +// } +// // process pushed message +// } +// +// The PubSubConn type wraps a Conn with convenience methods for implementing +// subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods +// send and flush a subscription management command. The receive method +// converts a pushed message to convenient types for use in a type switch. +// +// psc := redis.PubSubConn{Conn: c} +// psc.Subscribe("example") +// for { +// switch v := psc.Receive().(type) { +// case redis.Message: +// fmt.Printf("%s: message: %s\n", v.Channel, v.Data) +// case redis.Subscription: +// fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count) +// case error: +// return v +// } +// } +// +// Reply Helpers +// +// The Bool, Int, Bytes, String, Strings and Values functions convert a reply +// to a value of a specific type. To allow convenient wrapping of calls to the +// connection Do and Receive methods, the functions take a second argument of +// type error. If the error is non-nil, then the helper function returns the +// error. If the error is nil, the function converts the reply to the specified +// type: +// +// exists, err := redis.Bool(c.Do("EXISTS", "foo")) +// if err != nil { +// // handle error return from c.Do or type conversion error. +// } +// +// The Scan function converts elements of a array reply to Go types: +// +// var value1 int +// var value2 string +// reply, err := redis.Values(c.Do("MGET", "key1", "key2")) +// if err != nil { +// // handle error +// } +// if _, err := redis.Scan(reply, &value1, &value2); err != nil { +// // handle error +// } +// +// Errors +// +// Connection methods return error replies from the server as type redis.Error. +// +// Call the connection Err() method to determine if the connection encountered +// non-recoverable error such as a network error or protocol parsing error. If +// Err() returns a non-nil value, then the connection is not usable and should +// be closed. +package redis diff --git a/vendor/github.com/gomodule/redigo/redis/go17.go b/vendor/github.com/gomodule/redigo/redis/go17.go new file mode 100644 index 00000000000..5f36379113c --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/go17.go @@ -0,0 +1,29 @@ +// +build go1.7,!go1.8 + +package redis + +import "crypto/tls" + +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + return &tls.Config{ + Rand: cfg.Rand, + Time: cfg.Time, + Certificates: cfg.Certificates, + NameToCertificate: cfg.NameToCertificate, + GetCertificate: cfg.GetCertificate, + RootCAs: cfg.RootCAs, + NextProtos: cfg.NextProtos, + ServerName: cfg.ServerName, + ClientAuth: cfg.ClientAuth, + ClientCAs: cfg.ClientCAs, + InsecureSkipVerify: cfg.InsecureSkipVerify, + CipherSuites: cfg.CipherSuites, + PreferServerCipherSuites: cfg.PreferServerCipherSuites, + ClientSessionCache: cfg.ClientSessionCache, + MinVersion: cfg.MinVersion, + MaxVersion: cfg.MaxVersion, + CurvePreferences: cfg.CurvePreferences, + DynamicRecordSizingDisabled: cfg.DynamicRecordSizingDisabled, + Renegotiation: cfg.Renegotiation, + } +} diff --git a/vendor/github.com/gomodule/redigo/redis/go18.go b/vendor/github.com/gomodule/redigo/redis/go18.go new file mode 100644 index 00000000000..558363be39a --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/go18.go @@ -0,0 +1,9 @@ +// +build go1.8 + +package redis + +import "crypto/tls" + +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + return cfg.Clone() +} diff --git a/vendor/github.com/gomodule/redigo/redis/log.go b/vendor/github.com/gomodule/redigo/redis/log.go new file mode 100644 index 00000000000..ef8cd7a0239 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/log.go @@ -0,0 +1,146 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "bytes" + "fmt" + "log" + "time" +) + +var ( + _ ConnWithTimeout = (*loggingConn)(nil) +) + +// NewLoggingConn returns a logging wrapper around a connection. +func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn { + if prefix != "" { + prefix = prefix + "." + } + return &loggingConn{conn, logger, prefix, nil} +} + +//NewLoggingConnFilter returns a logging wrapper around a connection and a filter function. +func NewLoggingConnFilter(conn Conn, logger *log.Logger, prefix string, skip func(cmdName string) bool) Conn { + if prefix != "" { + prefix = prefix + "." + } + return &loggingConn{conn, logger, prefix, skip} +} + +type loggingConn struct { + Conn + logger *log.Logger + prefix string + skip func(cmdName string) bool +} + +func (c *loggingConn) Close() error { + err := c.Conn.Close() + var buf bytes.Buffer + fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err) + c.logger.Output(2, buf.String()) // nolint: errcheck + return err +} + +func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) { + const chop = 32 + switch v := v.(type) { + case []byte: + if len(v) > chop { + fmt.Fprintf(buf, "%q...", v[:chop]) + } else { + fmt.Fprintf(buf, "%q", v) + } + case string: + if len(v) > chop { + fmt.Fprintf(buf, "%q...", v[:chop]) + } else { + fmt.Fprintf(buf, "%q", v) + } + case []interface{}: + if len(v) == 0 { + buf.WriteString("[]") + } else { + sep := "[" + fin := "]" + if len(v) > chop { + v = v[:chop] + fin = "...]" + } + for _, vv := range v { + buf.WriteString(sep) + c.printValue(buf, vv) + sep = ", " + } + buf.WriteString(fin) + } + default: + fmt.Fprint(buf, v) + } +} + +func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) { + if c.skip != nil && c.skip(commandName) { + return + } + var buf bytes.Buffer + fmt.Fprintf(&buf, "%s%s(", c.prefix, method) + if method != "Receive" { + buf.WriteString(commandName) + for _, arg := range args { + buf.WriteString(", ") + c.printValue(&buf, arg) + } + } + buf.WriteString(") -> (") + if method != "Send" { + c.printValue(&buf, reply) + buf.WriteString(", ") + } + fmt.Fprintf(&buf, "%v)", err) + c.logger.Output(3, buf.String()) // nolint: errcheck +} + +func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) { + reply, err := c.Conn.Do(commandName, args...) + c.print("Do", commandName, args, reply, err) + return reply, err +} + +func (c *loggingConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (interface{}, error) { + reply, err := DoWithTimeout(c.Conn, timeout, commandName, args...) + c.print("DoWithTimeout", commandName, args, reply, err) + return reply, err +} + +func (c *loggingConn) Send(commandName string, args ...interface{}) error { + err := c.Conn.Send(commandName, args...) + c.print("Send", commandName, args, nil, err) + return err +} + +func (c *loggingConn) Receive() (interface{}, error) { + reply, err := c.Conn.Receive() + c.print("Receive", "", nil, reply, err) + return reply, err +} + +func (c *loggingConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) { + reply, err := ReceiveWithTimeout(c.Conn, timeout) + c.print("ReceiveWithTimeout", "", nil, reply, err) + return reply, err +} diff --git a/vendor/github.com/gomodule/redigo/redis/pool.go b/vendor/github.com/gomodule/redigo/redis/pool.go new file mode 100644 index 00000000000..c7a2f19435b --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/pool.go @@ -0,0 +1,636 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/sha1" + "errors" + "io" + "strconv" + "sync" + "time" +) + +var ( + _ ConnWithTimeout = (*activeConn)(nil) + _ ConnWithTimeout = (*errorConn)(nil) +) + +var nowFunc = time.Now // for testing + +// ErrPoolExhausted is returned from a pool connection method (Do, Send, +// Receive, Flush, Err) when the maximum number of database connections in the +// pool has been reached. +var ErrPoolExhausted = errors.New("redigo: connection pool exhausted") + +var ( + errConnClosed = errors.New("redigo: connection closed") +) + +// Pool maintains a pool of connections. The application calls the Get method +// to get a connection from the pool and the connection's Close method to +// return the connection's resources to the pool. +// +// The following example shows how to use a pool in a web application. The +// application creates a pool at application startup and makes it available to +// request handlers using a package level variable. The pool configuration used +// here is an example, not a recommendation. +// +// func newPool(addr string) *redis.Pool { +// return &redis.Pool{ +// MaxIdle: 3, +// IdleTimeout: 240 * time.Second, +// // Dial or DialContext must be set. When both are set, DialContext takes precedence over Dial. +// Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) }, +// } +// } +// +// var ( +// pool *redis.Pool +// redisServer = flag.String("redisServer", ":6379", "") +// ) +// +// func main() { +// flag.Parse() +// pool = newPool(*redisServer) +// ... +// } +// +// A request handler gets a connection from the pool and closes the connection +// when the handler is done: +// +// func serveHome(w http.ResponseWriter, r *http.Request) { +// conn := pool.Get() +// defer conn.Close() +// ... +// } +// +// Use the Dial function to authenticate connections with the AUTH command or +// select a database with the SELECT command: +// +// pool := &redis.Pool{ +// // Other pool configuration not shown in this example. +// Dial: func () (redis.Conn, error) { +// c, err := redis.Dial("tcp", server) +// if err != nil { +// return nil, err +// } +// if _, err := c.Do("AUTH", password); err != nil { +// c.Close() +// return nil, err +// } +// if _, err := c.Do("SELECT", db); err != nil { +// c.Close() +// return nil, err +// } +// return c, nil +// }, +// } +// +// Use the TestOnBorrow function to check the health of an idle connection +// before the connection is returned to the application. This example PINGs +// connections that have been idle more than a minute: +// +// pool := &redis.Pool{ +// // Other pool configuration not shown in this example. +// TestOnBorrow: func(c redis.Conn, t time.Time) error { +// if time.Since(t) < time.Minute { +// return nil +// } +// _, err := c.Do("PING") +// return err +// }, +// } +// +type Pool struct { + // Dial is an application supplied function for creating and configuring a + // connection. + // + // The connection returned from Dial must not be in a special state + // (subscribed to pubsub channel, transaction started, ...). + Dial func() (Conn, error) + + // DialContext is an application supplied function for creating and configuring a + // connection with the given context. + // + // The connection returned from Dial must not be in a special state + // (subscribed to pubsub channel, transaction started, ...). + DialContext func(ctx context.Context) (Conn, error) + + // TestOnBorrow is an optional application supplied function for checking + // the health of an idle connection before the connection is used again by + // the application. Argument t is the time that the connection was returned + // to the pool. If the function returns an error, then the connection is + // closed. + TestOnBorrow func(c Conn, t time.Time) error + + // Maximum number of idle connections in the pool. + MaxIdle int + + // Maximum number of connections allocated by the pool at a given time. + // When zero, there is no limit on the number of connections in the pool. + MaxActive int + + // Close connections after remaining idle for this duration. If the value + // is zero, then idle connections are not closed. Applications should set + // the timeout to a value less than the server's timeout. + IdleTimeout time.Duration + + // If Wait is true and the pool is at the MaxActive limit, then Get() waits + // for a connection to be returned to the pool before returning. + Wait bool + + // Close connections older than this duration. If the value is zero, then + // the pool does not close connections based on age. + MaxConnLifetime time.Duration + + mu sync.Mutex // mu protects the following fields + closed bool // set to true when the pool is closed. + active int // the number of open connections in the pool + initOnce sync.Once // the init ch once func + ch chan struct{} // limits open connections when p.Wait is true + idle idleList // idle connections + waitCount int64 // total number of connections waited for. + waitDuration time.Duration // total time waited for new connections. +} + +// NewPool creates a new pool. +// +// Deprecated: Initialize the Pool directly as shown in the example. +func NewPool(newFn func() (Conn, error), maxIdle int) *Pool { + return &Pool{Dial: newFn, MaxIdle: maxIdle} +} + +// Get gets a connection. The application must close the returned connection. +// This method always returns a valid connection so that applications can defer +// error handling to the first use of the connection. If there is an error +// getting an underlying connection, then the connection Err, Do, Send, Flush +// and Receive methods return that error. +func (p *Pool) Get() Conn { + // GetContext returns errorConn in the first argument when an error occurs. + c, _ := p.GetContext(context.Background()) + return c +} + +// GetContext gets a connection using the provided context. +// +// The provided Context must be non-nil. If the context expires before the +// connection is complete, an error is returned. Any expiration on the context +// will not affect the returned connection. +// +// If the function completes without error, then the application must close the +// returned connection. +func (p *Pool) GetContext(ctx context.Context) (Conn, error) { + // Wait until there is a vacant connection in the pool. + waited, err := p.waitVacantConn(ctx) + if err != nil { + return errorConn{err}, err + } + + p.mu.Lock() + + if waited > 0 { + p.waitCount++ + p.waitDuration += waited + } + + // Prune stale connections at the back of the idle list. + if p.IdleTimeout > 0 { + n := p.idle.count + for i := 0; i < n && p.idle.back != nil && p.idle.back.t.Add(p.IdleTimeout).Before(nowFunc()); i++ { + pc := p.idle.back + p.idle.popBack() + p.mu.Unlock() + pc.c.Close() + p.mu.Lock() + p.active-- + } + } + + // Get idle connection from the front of idle list. + for p.idle.front != nil { + pc := p.idle.front + p.idle.popFront() + p.mu.Unlock() + if (p.TestOnBorrow == nil || p.TestOnBorrow(pc.c, pc.t) == nil) && + (p.MaxConnLifetime == 0 || nowFunc().Sub(pc.created) < p.MaxConnLifetime) { + return &activeConn{p: p, pc: pc}, nil + } + pc.c.Close() + p.mu.Lock() + p.active-- + } + + // Check for pool closed before dialing a new connection. + if p.closed { + p.mu.Unlock() + err := errors.New("redigo: get on closed pool") + return errorConn{err}, err + } + + // Handle limit for p.Wait == false. + if !p.Wait && p.MaxActive > 0 && p.active >= p.MaxActive { + p.mu.Unlock() + return errorConn{ErrPoolExhausted}, ErrPoolExhausted + } + + p.active++ + p.mu.Unlock() + c, err := p.dial(ctx) + if err != nil { + p.mu.Lock() + p.active-- + if p.ch != nil && !p.closed { + p.ch <- struct{}{} + } + p.mu.Unlock() + return errorConn{err}, err + } + return &activeConn{p: p, pc: &poolConn{c: c, created: nowFunc()}}, nil +} + +// PoolStats contains pool statistics. +type PoolStats struct { + // ActiveCount is the number of connections in the pool. The count includes + // idle connections and connections in use. + ActiveCount int + // IdleCount is the number of idle connections in the pool. + IdleCount int + + // WaitCount is the total number of connections waited for. + // This value is currently not guaranteed to be 100% accurate. + WaitCount int64 + + // WaitDuration is the total time blocked waiting for a new connection. + // This value is currently not guaranteed to be 100% accurate. + WaitDuration time.Duration +} + +// Stats returns pool's statistics. +func (p *Pool) Stats() PoolStats { + p.mu.Lock() + stats := PoolStats{ + ActiveCount: p.active, + IdleCount: p.idle.count, + WaitCount: p.waitCount, + WaitDuration: p.waitDuration, + } + p.mu.Unlock() + + return stats +} + +// ActiveCount returns the number of connections in the pool. The count +// includes idle connections and connections in use. +func (p *Pool) ActiveCount() int { + p.mu.Lock() + active := p.active + p.mu.Unlock() + return active +} + +// IdleCount returns the number of idle connections in the pool. +func (p *Pool) IdleCount() int { + p.mu.Lock() + idle := p.idle.count + p.mu.Unlock() + return idle +} + +// Close releases the resources used by the pool. +func (p *Pool) Close() error { + p.mu.Lock() + if p.closed { + p.mu.Unlock() + return nil + } + p.closed = true + p.active -= p.idle.count + pc := p.idle.front + p.idle.count = 0 + p.idle.front, p.idle.back = nil, nil + if p.ch != nil { + close(p.ch) + } + p.mu.Unlock() + for ; pc != nil; pc = pc.next { + pc.c.Close() + } + return nil +} + +func (p *Pool) lazyInit() { + p.initOnce.Do(func() { + p.ch = make(chan struct{}, p.MaxActive) + if p.closed { + close(p.ch) + } else { + for i := 0; i < p.MaxActive; i++ { + p.ch <- struct{}{} + } + } + }) +} + +// waitVacantConn waits for a vacant connection in pool if waiting +// is enabled and pool size is limited, otherwise returns instantly. +// If ctx expires before that, an error is returned. +// +// If there were no vacant connection in the pool right away it returns the time spent waiting +// for that connection to appear in the pool. +func (p *Pool) waitVacantConn(ctx context.Context) (waited time.Duration, err error) { + if !p.Wait || p.MaxActive <= 0 { + // No wait or no connection limit. + return 0, nil + } + + p.lazyInit() + + // wait indicates if we believe it will block so its not 100% accurate + // however for stats it should be good enough. + wait := len(p.ch) == 0 + var start time.Time + if wait { + start = time.Now() + } + + select { + case <-p.ch: + // Additionally check that context hasn't expired while we were waiting, + // because `select` picks a random `case` if several of them are "ready". + select { + case <-ctx.Done(): + p.ch <- struct{}{} + return 0, ctx.Err() + default: + } + case <-ctx.Done(): + return 0, ctx.Err() + } + + if wait { + return time.Since(start), nil + } + return 0, nil +} + +func (p *Pool) dial(ctx context.Context) (Conn, error) { + if p.DialContext != nil { + return p.DialContext(ctx) + } + if p.Dial != nil { + return p.Dial() + } + return nil, errors.New("redigo: must pass Dial or DialContext to pool") +} + +func (p *Pool) put(pc *poolConn, forceClose bool) error { + p.mu.Lock() + if !p.closed && !forceClose { + pc.t = nowFunc() + p.idle.pushFront(pc) + if p.idle.count > p.MaxIdle { + pc = p.idle.back + p.idle.popBack() + } else { + pc = nil + } + } + + if pc != nil { + p.mu.Unlock() + pc.c.Close() + p.mu.Lock() + p.active-- + } + + if p.ch != nil && !p.closed { + p.ch <- struct{}{} + } + p.mu.Unlock() + return nil +} + +type activeConn struct { + p *Pool + pc *poolConn + state int +} + +var ( + sentinel []byte + sentinelOnce sync.Once +) + +func initSentinel() { + p := make([]byte, 64) + if _, err := rand.Read(p); err == nil { + sentinel = p + } else { + h := sha1.New() + io.WriteString(h, "Oops, rand failed. Use time instead.") // nolint: errcheck + io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10)) // nolint: errcheck + sentinel = h.Sum(nil) + } +} + +func (ac *activeConn) firstError(errs ...error) error { + for _, err := range errs[:len(errs)-1] { + if err != nil { + return err + } + } + return errs[len(errs)-1] +} + +func (ac *activeConn) Close() (err error) { + pc := ac.pc + if pc == nil { + return nil + } + ac.pc = nil + + if ac.state&connectionMultiState != 0 { + err = pc.c.Send("DISCARD") + ac.state &^= (connectionMultiState | connectionWatchState) + } else if ac.state&connectionWatchState != 0 { + err = pc.c.Send("UNWATCH") + ac.state &^= connectionWatchState + } + if ac.state&connectionSubscribeState != 0 { + err = ac.firstError(err, + pc.c.Send("UNSUBSCRIBE"), + pc.c.Send("PUNSUBSCRIBE"), + ) + // To detect the end of the message stream, ask the server to echo + // a sentinel value and read until we see that value. + sentinelOnce.Do(initSentinel) + err = ac.firstError(err, + pc.c.Send("ECHO", sentinel), + pc.c.Flush(), + ) + for { + p, err2 := pc.c.Receive() + if err2 != nil { + err = ac.firstError(err, err2) + break + } + if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) { + ac.state &^= connectionSubscribeState + break + } + } + } + _, err2 := pc.c.Do("") + return ac.firstError( + err, + err2, + ac.p.put(pc, ac.state != 0 || pc.c.Err() != nil), + ) +} + +func (ac *activeConn) Err() error { + pc := ac.pc + if pc == nil { + return errConnClosed + } + return pc.c.Err() +} + +func (ac *activeConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) { + pc := ac.pc + if pc == nil { + return nil, errConnClosed + } + ci := lookupCommandInfo(commandName) + ac.state = (ac.state | ci.Set) &^ ci.Clear + return pc.c.Do(commandName, args...) +} + +func (ac *activeConn) DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error) { + pc := ac.pc + if pc == nil { + return nil, errConnClosed + } + cwt, ok := pc.c.(ConnWithTimeout) + if !ok { + return nil, errTimeoutNotSupported + } + ci := lookupCommandInfo(commandName) + ac.state = (ac.state | ci.Set) &^ ci.Clear + return cwt.DoWithTimeout(timeout, commandName, args...) +} + +func (ac *activeConn) Send(commandName string, args ...interface{}) error { + pc := ac.pc + if pc == nil { + return errConnClosed + } + ci := lookupCommandInfo(commandName) + ac.state = (ac.state | ci.Set) &^ ci.Clear + return pc.c.Send(commandName, args...) +} + +func (ac *activeConn) Flush() error { + pc := ac.pc + if pc == nil { + return errConnClosed + } + return pc.c.Flush() +} + +func (ac *activeConn) Receive() (reply interface{}, err error) { + pc := ac.pc + if pc == nil { + return nil, errConnClosed + } + return pc.c.Receive() +} + +func (ac *activeConn) ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) { + pc := ac.pc + if pc == nil { + return nil, errConnClosed + } + cwt, ok := pc.c.(ConnWithTimeout) + if !ok { + return nil, errTimeoutNotSupported + } + return cwt.ReceiveWithTimeout(timeout) +} + +type errorConn struct{ err error } + +func (ec errorConn) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err } +func (ec errorConn) DoWithTimeout(time.Duration, string, ...interface{}) (interface{}, error) { + return nil, ec.err +} +func (ec errorConn) Send(string, ...interface{}) error { return ec.err } +func (ec errorConn) Err() error { return ec.err } +func (ec errorConn) Close() error { return nil } +func (ec errorConn) Flush() error { return ec.err } +func (ec errorConn) Receive() (interface{}, error) { return nil, ec.err } +func (ec errorConn) ReceiveWithTimeout(time.Duration) (interface{}, error) { return nil, ec.err } + +type idleList struct { + count int + front, back *poolConn +} + +type poolConn struct { + c Conn + t time.Time + created time.Time + next, prev *poolConn +} + +func (l *idleList) pushFront(pc *poolConn) { + pc.next = l.front + pc.prev = nil + if l.count == 0 { + l.back = pc + } else { + l.front.prev = pc + } + l.front = pc + l.count++ +} + +func (l *idleList) popFront() { + pc := l.front + l.count-- + if l.count == 0 { + l.front, l.back = nil, nil + } else { + pc.next.prev = nil + l.front = pc.next + } + pc.next, pc.prev = nil, nil +} + +func (l *idleList) popBack() { + pc := l.back + l.count-- + if l.count == 0 { + l.front, l.back = nil, nil + } else { + pc.prev.next = nil + l.back = pc.prev + } + pc.next, pc.prev = nil, nil +} diff --git a/vendor/github.com/gomodule/redigo/redis/pubsub.go b/vendor/github.com/gomodule/redigo/redis/pubsub.go new file mode 100644 index 00000000000..cc58575760a --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/pubsub.go @@ -0,0 +1,158 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "errors" + "time" +) + +// Subscription represents a subscribe or unsubscribe notification. +type Subscription struct { + // Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe" + Kind string + + // The channel that was changed. + Channel string + + // The current number of subscriptions for connection. + Count int +} + +// Message represents a message notification. +type Message struct { + // The originating channel. + Channel string + + // The matched pattern, if any + Pattern string + + // The message data. + Data []byte +} + +// Pong represents a pubsub pong notification. +type Pong struct { + Data string +} + +// PubSubConn wraps a Conn with convenience methods for subscribers. +type PubSubConn struct { + Conn Conn +} + +// Close closes the connection. +func (c PubSubConn) Close() error { + return c.Conn.Close() +} + +// Subscribe subscribes the connection to the specified channels. +func (c PubSubConn) Subscribe(channel ...interface{}) error { + if err := c.Conn.Send("SUBSCRIBE", channel...); err != nil { + return err + } + return c.Conn.Flush() +} + +// PSubscribe subscribes the connection to the given patterns. +func (c PubSubConn) PSubscribe(channel ...interface{}) error { + if err := c.Conn.Send("PSUBSCRIBE", channel...); err != nil { + return err + } + return c.Conn.Flush() +} + +// Unsubscribe unsubscribes the connection from the given channels, or from all +// of them if none is given. +func (c PubSubConn) Unsubscribe(channel ...interface{}) error { + if err := c.Conn.Send("UNSUBSCRIBE", channel...); err != nil { + return err + } + return c.Conn.Flush() +} + +// PUnsubscribe unsubscribes the connection from the given patterns, or from all +// of them if none is given. +func (c PubSubConn) PUnsubscribe(channel ...interface{}) error { + if err := c.Conn.Send("PUNSUBSCRIBE", channel...); err != nil { + return err + } + return c.Conn.Flush() +} + +// Ping sends a PING to the server with the specified data. +// +// The connection must be subscribed to at least one channel or pattern when +// calling this method. +func (c PubSubConn) Ping(data string) error { + if err := c.Conn.Send("PING", data); err != nil { + return err + } + return c.Conn.Flush() +} + +// Receive returns a pushed message as a Subscription, Message, Pong or error. +// The return value is intended to be used directly in a type switch as +// illustrated in the PubSubConn example. +func (c PubSubConn) Receive() interface{} { + return c.receiveInternal(c.Conn.Receive()) +} + +// ReceiveWithTimeout is like Receive, but it allows the application to +// override the connection's default timeout. +func (c PubSubConn) ReceiveWithTimeout(timeout time.Duration) interface{} { + return c.receiveInternal(ReceiveWithTimeout(c.Conn, timeout)) +} + +func (c PubSubConn) receiveInternal(replyArg interface{}, errArg error) interface{} { + reply, err := Values(replyArg, errArg) + if err != nil { + return err + } + + var kind string + reply, err = Scan(reply, &kind) + if err != nil { + return err + } + + switch kind { + case "message": + var m Message + if _, err := Scan(reply, &m.Channel, &m.Data); err != nil { + return err + } + return m + case "pmessage": + var m Message + if _, err := Scan(reply, &m.Pattern, &m.Channel, &m.Data); err != nil { + return err + } + return m + case "subscribe", "psubscribe", "unsubscribe", "punsubscribe": + s := Subscription{Kind: kind} + if _, err := Scan(reply, &s.Channel, &s.Count); err != nil { + return err + } + return s + case "pong": + var p Pong + if _, err := Scan(reply, &p.Data); err != nil { + return err + } + return p + } + return errors.New("redigo: unknown pubsub notification") +} diff --git a/vendor/github.com/gomodule/redigo/redis/redis.go b/vendor/github.com/gomodule/redigo/redis/redis.go new file mode 100644 index 00000000000..e4464874471 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/redis.go @@ -0,0 +1,138 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "errors" + "time" +) + +// Error represents an error returned in a command reply. +type Error string + +func (err Error) Error() string { return string(err) } + +// Conn represents a connection to a Redis server. +type Conn interface { + // Close closes the connection. + Close() error + + // Err returns a non-nil value when the connection is not usable. + Err() error + + // Do sends a command to the server and returns the received reply. + Do(commandName string, args ...interface{}) (reply interface{}, err error) + + // Send writes the command to the client's output buffer. + Send(commandName string, args ...interface{}) error + + // Flush flushes the output buffer to the Redis server. + Flush() error + + // Receive receives a single reply from the Redis server + Receive() (reply interface{}, err error) +} + +// Argument is the interface implemented by an object which wants to control how +// the object is converted to Redis bulk strings. +type Argument interface { + // RedisArg returns a value to be encoded as a bulk string per the + // conversions listed in the section 'Executing Commands'. + // Implementations should typically return a []byte or string. + RedisArg() interface{} +} + +// Scanner is implemented by an object which wants to control its value is +// interpreted when read from Redis. +type Scanner interface { + // RedisScan assigns a value from a Redis value. The argument src is one of + // the reply types listed in the section `Executing Commands`. + // + // An error should be returned if the value cannot be stored without + // loss of information. + RedisScan(src interface{}) error +} + +// ConnWithTimeout is an optional interface that allows the caller to override +// a connection's default read timeout. This interface is useful for executing +// the BLPOP, BRPOP, BRPOPLPUSH, XREAD and other commands that block at the +// server. +// +// A connection's default read timeout is set with the DialReadTimeout dial +// option. Applications should rely on the default timeout for commands that do +// not block at the server. +// +// All of the Conn implementations in this package satisfy the ConnWithTimeout +// interface. +// +// Use the DoWithTimeout and ReceiveWithTimeout helper functions to simplify +// use of this interface. +type ConnWithTimeout interface { + Conn + + // Do sends a command to the server and returns the received reply. + // The timeout overrides the read timeout set when dialing the + // connection. + DoWithTimeout(timeout time.Duration, commandName string, args ...interface{}) (reply interface{}, err error) + + // Receive receives a single reply from the Redis server. The timeout + // overrides the read timeout set when dialing the connection. + ReceiveWithTimeout(timeout time.Duration) (reply interface{}, err error) +} + +var errTimeoutNotSupported = errors.New("redis: connection does not support ConnWithTimeout") + +// DoWithTimeout executes a Redis command with the specified read timeout. If +// the connection does not satisfy the ConnWithTimeout interface, then an error +// is returned. +func DoWithTimeout(c Conn, timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) { + cwt, ok := c.(ConnWithTimeout) + if !ok { + return nil, errTimeoutNotSupported + } + return cwt.DoWithTimeout(timeout, cmd, args...) +} + +// ReceiveWithTimeout receives a reply with the specified read timeout. If the +// connection does not satisfy the ConnWithTimeout interface, then an error is +// returned. +func ReceiveWithTimeout(c Conn, timeout time.Duration) (interface{}, error) { + cwt, ok := c.(ConnWithTimeout) + if !ok { + return nil, errTimeoutNotSupported + } + return cwt.ReceiveWithTimeout(timeout) +} + +// SlowLog represents a redis SlowLog +type SlowLog struct { + // ID is a unique progressive identifier for every slow log entry. + ID int64 + + // Time is the unix timestamp at which the logged command was processed. + Time time.Time + + // ExecutationTime is the amount of time needed for the command execution. + ExecutionTime time.Duration + + // Args is the command name and arguments + Args []string + + // ClientAddr is the client IP address (4.0 only). + ClientAddr string + + // ClientName is the name set via the CLIENT SETNAME command (4.0 only). + ClientName string +} diff --git a/vendor/github.com/gomodule/redigo/redis/reply.go b/vendor/github.com/gomodule/redigo/redis/reply.go new file mode 100644 index 00000000000..dfe6aff7938 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/reply.go @@ -0,0 +1,583 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "errors" + "fmt" + "strconv" + "time" +) + +// ErrNil indicates that a reply value is nil. +var ErrNil = errors.New("redigo: nil returned") + +// Int is a helper that converts a command reply to an integer. If err is not +// equal to nil, then Int returns 0, err. Otherwise, Int converts the +// reply to an int as follows: +// +// Reply type Result +// integer int(reply), nil +// bulk string parsed reply, nil +// nil 0, ErrNil +// other 0, error +func Int(reply interface{}, err error) (int, error) { + if err != nil { + return 0, err + } + switch reply := reply.(type) { + case int64: + x := int(reply) + if int64(x) != reply { + return 0, strconv.ErrRange + } + return x, nil + case []byte: + n, err := strconv.ParseInt(string(reply), 10, 0) + return int(n), err + case nil: + return 0, ErrNil + case Error: + return 0, reply + } + return 0, fmt.Errorf("redigo: unexpected type for Int, got type %T", reply) +} + +// Int64 is a helper that converts a command reply to 64 bit integer. If err is +// not equal to nil, then Int64 returns 0, err. Otherwise, Int64 converts the +// reply to an int64 as follows: +// +// Reply type Result +// integer reply, nil +// bulk string parsed reply, nil +// nil 0, ErrNil +// other 0, error +func Int64(reply interface{}, err error) (int64, error) { + if err != nil { + return 0, err + } + switch reply := reply.(type) { + case int64: + return reply, nil + case []byte: + n, err := strconv.ParseInt(string(reply), 10, 64) + return n, err + case nil: + return 0, ErrNil + case Error: + return 0, reply + } + return 0, fmt.Errorf("redigo: unexpected type for Int64, got type %T", reply) +} + +func errNegativeInt(v int64) error { + return fmt.Errorf("redigo: unexpected negative value %v for Uint64", v) +} + +// Uint64 is a helper that converts a command reply to 64 bit unsigned integer. +// If err is not equal to nil, then Uint64 returns 0, err. Otherwise, Uint64 converts the +// reply to an uint64 as follows: +// +// Reply type Result +// +integer reply, nil +// bulk string parsed reply, nil +// nil 0, ErrNil +// other 0, error +func Uint64(reply interface{}, err error) (uint64, error) { + if err != nil { + return 0, err + } + switch reply := reply.(type) { + case int64: + if reply < 0 { + return 0, errNegativeInt(reply) + } + return uint64(reply), nil + case []byte: + n, err := strconv.ParseUint(string(reply), 10, 64) + return n, err + case nil: + return 0, ErrNil + case Error: + return 0, reply + } + return 0, fmt.Errorf("redigo: unexpected type for Uint64, got type %T", reply) +} + +// Float64 is a helper that converts a command reply to 64 bit float. If err is +// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts +// the reply to a float64 as follows: +// +// Reply type Result +// bulk string parsed reply, nil +// nil 0, ErrNil +// other 0, error +func Float64(reply interface{}, err error) (float64, error) { + if err != nil { + return 0, err + } + switch reply := reply.(type) { + case []byte: + n, err := strconv.ParseFloat(string(reply), 64) + return n, err + case nil: + return 0, ErrNil + case Error: + return 0, reply + } + return 0, fmt.Errorf("redigo: unexpected type for Float64, got type %T", reply) +} + +// String is a helper that converts a command reply to a string. If err is not +// equal to nil, then String returns "", err. Otherwise String converts the +// reply to a string as follows: +// +// Reply type Result +// bulk string string(reply), nil +// simple string reply, nil +// nil "", ErrNil +// other "", error +func String(reply interface{}, err error) (string, error) { + if err != nil { + return "", err + } + switch reply := reply.(type) { + case []byte: + return string(reply), nil + case string: + return reply, nil + case nil: + return "", ErrNil + case Error: + return "", reply + } + return "", fmt.Errorf("redigo: unexpected type for String, got type %T", reply) +} + +// Bytes is a helper that converts a command reply to a slice of bytes. If err +// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts +// the reply to a slice of bytes as follows: +// +// Reply type Result +// bulk string reply, nil +// simple string []byte(reply), nil +// nil nil, ErrNil +// other nil, error +func Bytes(reply interface{}, err error) ([]byte, error) { + if err != nil { + return nil, err + } + switch reply := reply.(type) { + case []byte: + return reply, nil + case string: + return []byte(reply), nil + case nil: + return nil, ErrNil + case Error: + return nil, reply + } + return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply) +} + +// Bool is a helper that converts a command reply to a boolean. If err is not +// equal to nil, then Bool returns false, err. Otherwise Bool converts the +// reply to boolean as follows: +// +// Reply type Result +// integer value != 0, nil +// bulk string strconv.ParseBool(reply) +// nil false, ErrNil +// other false, error +func Bool(reply interface{}, err error) (bool, error) { + if err != nil { + return false, err + } + switch reply := reply.(type) { + case int64: + return reply != 0, nil + case []byte: + return strconv.ParseBool(string(reply)) + case nil: + return false, ErrNil + case Error: + return false, reply + } + return false, fmt.Errorf("redigo: unexpected type for Bool, got type %T", reply) +} + +// MultiBulk is a helper that converts an array command reply to a []interface{}. +// +// Deprecated: Use Values instead. +func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) } + +// Values is a helper that converts an array command reply to a []interface{}. +// If err is not equal to nil, then Values returns nil, err. Otherwise, Values +// converts the reply as follows: +// +// Reply type Result +// array reply, nil +// nil nil, ErrNil +// other nil, error +func Values(reply interface{}, err error) ([]interface{}, error) { + if err != nil { + return nil, err + } + switch reply := reply.(type) { + case []interface{}: + return reply, nil + case nil: + return nil, ErrNil + case Error: + return nil, reply + } + return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply) +} + +func sliceHelper(reply interface{}, err error, name string, makeSlice func(int), assign func(int, interface{}) error) error { + if err != nil { + return err + } + switch reply := reply.(type) { + case []interface{}: + makeSlice(len(reply)) + for i := range reply { + if reply[i] == nil { + continue + } + if err := assign(i, reply[i]); err != nil { + return err + } + } + return nil + case nil: + return ErrNil + case Error: + return reply + } + return fmt.Errorf("redigo: unexpected type for %s, got type %T", name, reply) +} + +// Float64s is a helper that converts an array command reply to a []float64. If +// err is not equal to nil, then Float64s returns nil, err. Nil array items are +// converted to 0 in the output slice. Floats64 returns an error if an array +// item is not a bulk string or nil. +func Float64s(reply interface{}, err error) ([]float64, error) { + var result []float64 + err = sliceHelper(reply, err, "Float64s", func(n int) { result = make([]float64, n) }, func(i int, v interface{}) error { + p, ok := v.([]byte) + if !ok { + return fmt.Errorf("redigo: unexpected element type for Floats64, got type %T", v) + } + f, err := strconv.ParseFloat(string(p), 64) + result[i] = f + return err + }) + return result, err +} + +// Strings is a helper that converts an array command reply to a []string. If +// err is not equal to nil, then Strings returns nil, err. Nil array items are +// converted to "" in the output slice. Strings returns an error if an array +// item is not a bulk string or nil. +func Strings(reply interface{}, err error) ([]string, error) { + var result []string + err = sliceHelper(reply, err, "Strings", func(n int) { result = make([]string, n) }, func(i int, v interface{}) error { + switch v := v.(type) { + case string: + result[i] = v + return nil + case []byte: + result[i] = string(v) + return nil + default: + return fmt.Errorf("redigo: unexpected element type for Strings, got type %T", v) + } + }) + return result, err +} + +// ByteSlices is a helper that converts an array command reply to a [][]byte. +// If err is not equal to nil, then ByteSlices returns nil, err. Nil array +// items are stay nil. ByteSlices returns an error if an array item is not a +// bulk string or nil. +func ByteSlices(reply interface{}, err error) ([][]byte, error) { + var result [][]byte + err = sliceHelper(reply, err, "ByteSlices", func(n int) { result = make([][]byte, n) }, func(i int, v interface{}) error { + p, ok := v.([]byte) + if !ok { + return fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", v) + } + result[i] = p + return nil + }) + return result, err +} + +// Int64s is a helper that converts an array command reply to a []int64. +// If err is not equal to nil, then Int64s returns nil, err. Nil array +// items are stay nil. Int64s returns an error if an array item is not a +// bulk string or nil. +func Int64s(reply interface{}, err error) ([]int64, error) { + var result []int64 + err = sliceHelper(reply, err, "Int64s", func(n int) { result = make([]int64, n) }, func(i int, v interface{}) error { + switch v := v.(type) { + case int64: + result[i] = v + return nil + case []byte: + n, err := strconv.ParseInt(string(v), 10, 64) + result[i] = n + return err + default: + return fmt.Errorf("redigo: unexpected element type for Int64s, got type %T", v) + } + }) + return result, err +} + +// Ints is a helper that converts an array command reply to a []int. +// If err is not equal to nil, then Ints returns nil, err. Nil array +// items are stay nil. Ints returns an error if an array item is not a +// bulk string or nil. +func Ints(reply interface{}, err error) ([]int, error) { + var result []int + err = sliceHelper(reply, err, "Ints", func(n int) { result = make([]int, n) }, func(i int, v interface{}) error { + switch v := v.(type) { + case int64: + n := int(v) + if int64(n) != v { + return strconv.ErrRange + } + result[i] = n + return nil + case []byte: + n, err := strconv.Atoi(string(v)) + result[i] = n + return err + default: + return fmt.Errorf("redigo: unexpected element type for Ints, got type %T", v) + } + }) + return result, err +} + +// StringMap is a helper that converts an array of strings (alternating key, value) +// into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format. +// Requires an even number of values in result. +func StringMap(result interface{}, err error) (map[string]string, error) { + values, err := Values(result, err) + if err != nil { + return nil, err + } + if len(values)%2 != 0 { + return nil, errors.New("redigo: StringMap expects even number of values result") + } + m := make(map[string]string, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, okKey := values[i].([]byte) + value, okValue := values[i+1].([]byte) + if !okKey || !okValue { + return nil, errors.New("redigo: StringMap key not a bulk string value") + } + m[string(key)] = string(value) + } + return m, nil +} + +// IntMap is a helper that converts an array of strings (alternating key, value) +// into a map[string]int. The HGETALL commands return replies in this format. +// Requires an even number of values in result. +func IntMap(result interface{}, err error) (map[string]int, error) { + values, err := Values(result, err) + if err != nil { + return nil, err + } + if len(values)%2 != 0 { + return nil, errors.New("redigo: IntMap expects even number of values result") + } + m := make(map[string]int, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].([]byte) + if !ok { + return nil, errors.New("redigo: IntMap key not a bulk string value") + } + value, err := Int(values[i+1], nil) + if err != nil { + return nil, err + } + m[string(key)] = value + } + return m, nil +} + +// Int64Map is a helper that converts an array of strings (alternating key, value) +// into a map[string]int64. The HGETALL commands return replies in this format. +// Requires an even number of values in result. +func Int64Map(result interface{}, err error) (map[string]int64, error) { + values, err := Values(result, err) + if err != nil { + return nil, err + } + if len(values)%2 != 0 { + return nil, errors.New("redigo: Int64Map expects even number of values result") + } + m := make(map[string]int64, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].([]byte) + if !ok { + return nil, errors.New("redigo: Int64Map key not a bulk string value") + } + value, err := Int64(values[i+1], nil) + if err != nil { + return nil, err + } + m[string(key)] = value + } + return m, nil +} + +// Positions is a helper that converts an array of positions (lat, long) +// into a [][2]float64. The GEOPOS command returns replies in this format. +func Positions(result interface{}, err error) ([]*[2]float64, error) { + values, err := Values(result, err) + if err != nil { + return nil, err + } + positions := make([]*[2]float64, len(values)) + for i := range values { + if values[i] == nil { + continue + } + p, ok := values[i].([]interface{}) + if !ok { + return nil, fmt.Errorf("redigo: unexpected element type for interface slice, got type %T", values[i]) + } + if len(p) != 2 { + return nil, fmt.Errorf("redigo: unexpected number of values for a member position, got %d", len(p)) + } + lat, err := Float64(p[0], nil) + if err != nil { + return nil, err + } + long, err := Float64(p[1], nil) + if err != nil { + return nil, err + } + positions[i] = &[2]float64{lat, long} + } + return positions, nil +} + +// Uint64s is a helper that converts an array command reply to a []uint64. +// If err is not equal to nil, then Uint64s returns nil, err. Nil array +// items are stay nil. Uint64s returns an error if an array item is not a +// bulk string or nil. +func Uint64s(reply interface{}, err error) ([]uint64, error) { + var result []uint64 + err = sliceHelper(reply, err, "Uint64s", func(n int) { result = make([]uint64, n) }, func(i int, v interface{}) error { + switch v := v.(type) { + case uint64: + result[i] = v + return nil + case []byte: + n, err := strconv.ParseUint(string(v), 10, 64) + result[i] = n + return err + default: + return fmt.Errorf("redigo: unexpected element type for Uint64s, got type %T", v) + } + }) + return result, err +} + +// Uint64Map is a helper that converts an array of strings (alternating key, value) +// into a map[string]uint64. The HGETALL commands return replies in this format. +// Requires an even number of values in result. +func Uint64Map(result interface{}, err error) (map[string]uint64, error) { + values, err := Values(result, err) + if err != nil { + return nil, err + } + if len(values)%2 != 0 { + return nil, errors.New("redigo: Uint64Map expects even number of values result") + } + m := make(map[string]uint64, len(values)/2) + for i := 0; i < len(values); i += 2 { + key, ok := values[i].([]byte) + if !ok { + return nil, errors.New("redigo: Uint64Map key not a bulk string value") + } + value, err := Uint64(values[i+1], nil) + if err != nil { + return nil, err + } + m[string(key)] = value + } + return m, nil +} + +// SlowLogs is a helper that parse the SLOWLOG GET command output and +// return the array of SlowLog +func SlowLogs(result interface{}, err error) ([]SlowLog, error) { + rawLogs, err := Values(result, err) + if err != nil { + return nil, err + } + logs := make([]SlowLog, len(rawLogs)) + for i, rawLog := range rawLogs { + rawLog, ok := rawLog.([]interface{}) + if !ok { + return nil, errors.New("redigo: slowlog element is not an array") + } + + var log SlowLog + + if len(rawLog) < 4 { + return nil, errors.New("redigo: slowlog element has less than four elements") + } + log.ID, ok = rawLog[0].(int64) + if !ok { + return nil, errors.New("redigo: slowlog element[0] not an int64") + } + timestamp, ok := rawLog[1].(int64) + if !ok { + return nil, errors.New("redigo: slowlog element[1] not an int64") + } + log.Time = time.Unix(timestamp, 0) + duration, ok := rawLog[2].(int64) + if !ok { + return nil, errors.New("redigo: slowlog element[2] not an int64") + } + log.ExecutionTime = time.Duration(duration) * time.Microsecond + + log.Args, err = Strings(rawLog[3], nil) + if err != nil { + return nil, fmt.Errorf("redigo: slowlog element[3] is not array of string. actual error is : %s", err.Error()) + } + if len(rawLog) >= 6 { + log.ClientAddr, err = String(rawLog[4], nil) + if err != nil { + return nil, fmt.Errorf("redigo: slowlog element[4] is not a string. actual error is : %s", err.Error()) + } + log.ClientName, err = String(rawLog[5], nil) + if err != nil { + return nil, fmt.Errorf("redigo: slowlog element[5] is not a string. actual error is : %s", err.Error()) + } + } + logs[i] = log + } + return logs, nil +} diff --git a/vendor/github.com/gomodule/redigo/redis/scan.go b/vendor/github.com/gomodule/redigo/redis/scan.go new file mode 100644 index 00000000000..379206edea5 --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/scan.go @@ -0,0 +1,683 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "sync" +) + +var ( + scannerType = reflect.TypeOf((*Scanner)(nil)).Elem() +) + +func ensureLen(d reflect.Value, n int) { + if n > d.Cap() { + d.Set(reflect.MakeSlice(d.Type(), n, n)) + } else { + d.SetLen(n) + } +} + +func cannotConvert(d reflect.Value, s interface{}) error { + var sname string + switch s.(type) { + case string: + sname = "Redis simple string" + case Error: + sname = "Redis error" + case int64: + sname = "Redis integer" + case []byte: + sname = "Redis bulk string" + case []interface{}: + sname = "Redis array" + case nil: + sname = "Redis nil" + default: + sname = reflect.TypeOf(s).String() + } + return fmt.Errorf("cannot convert from %s to %s", sname, d.Type()) +} + +func convertAssignNil(d reflect.Value) (err error) { + switch d.Type().Kind() { + case reflect.Slice, reflect.Interface: + d.Set(reflect.Zero(d.Type())) + default: + err = cannotConvert(d, nil) + } + return err +} + +func convertAssignError(d reflect.Value, s Error) (err error) { + if d.Kind() == reflect.String { + d.SetString(string(s)) + } else if d.Kind() == reflect.Slice && d.Type().Elem().Kind() == reflect.Uint8 { + d.SetBytes([]byte(s)) + } else { + err = cannotConvert(d, s) + } + return +} + +func convertAssignString(d reflect.Value, s string) (err error) { + switch d.Type().Kind() { + case reflect.Float32, reflect.Float64: + var x float64 + x, err = strconv.ParseFloat(s, d.Type().Bits()) + d.SetFloat(x) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + var x int64 + x, err = strconv.ParseInt(s, 10, d.Type().Bits()) + d.SetInt(x) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + var x uint64 + x, err = strconv.ParseUint(s, 10, d.Type().Bits()) + d.SetUint(x) + case reflect.Bool: + var x bool + x, err = strconv.ParseBool(s) + d.SetBool(x) + case reflect.String: + d.SetString(s) + case reflect.Slice: + if d.Type().Elem().Kind() == reflect.Uint8 { + d.SetBytes([]byte(s)) + } else { + err = cannotConvert(d, s) + } + case reflect.Ptr: + err = convertAssignString(d.Elem(), s) + default: + err = cannotConvert(d, s) + } + return +} + +func convertAssignBulkString(d reflect.Value, s []byte) (err error) { + switch d.Type().Kind() { + case reflect.Slice: + // Handle []byte destination here to avoid unnecessary + // []byte -> string -> []byte converion. + if d.Type().Elem().Kind() == reflect.Uint8 { + d.SetBytes(s) + } else { + err = cannotConvert(d, s) + } + case reflect.Ptr: + if d.CanInterface() && d.CanSet() { + if s == nil { + if d.IsNil() { + return nil + } + + d.Set(reflect.Zero(d.Type())) + return nil + } + + if d.IsNil() { + d.Set(reflect.New(d.Type().Elem())) + } + + if sc, ok := d.Interface().(Scanner); ok { + return sc.RedisScan(s) + } + } + err = convertAssignString(d, string(s)) + default: + err = convertAssignString(d, string(s)) + } + return err +} + +func convertAssignInt(d reflect.Value, s int64) (err error) { + switch d.Type().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + d.SetInt(s) + if d.Int() != s { + err = strconv.ErrRange + d.SetInt(0) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if s < 0 { + err = strconv.ErrRange + } else { + x := uint64(s) + d.SetUint(x) + if d.Uint() != x { + err = strconv.ErrRange + d.SetUint(0) + } + } + case reflect.Bool: + d.SetBool(s != 0) + default: + err = cannotConvert(d, s) + } + return +} + +func convertAssignValue(d reflect.Value, s interface{}) (err error) { + if d.Kind() != reflect.Ptr { + if d.CanAddr() { + d2 := d.Addr() + if d2.CanInterface() { + if scanner, ok := d2.Interface().(Scanner); ok { + return scanner.RedisScan(s) + } + } + } + } else if d.CanInterface() { + // Already a reflect.Ptr + if d.IsNil() { + d.Set(reflect.New(d.Type().Elem())) + } + if scanner, ok := d.Interface().(Scanner); ok { + return scanner.RedisScan(s) + } + } + + switch s := s.(type) { + case nil: + err = convertAssignNil(d) + case []byte: + err = convertAssignBulkString(d, s) + case int64: + err = convertAssignInt(d, s) + case string: + err = convertAssignString(d, s) + case Error: + err = convertAssignError(d, s) + default: + err = cannotConvert(d, s) + } + return err +} + +func convertAssignArray(d reflect.Value, s []interface{}) error { + if d.Type().Kind() != reflect.Slice { + return cannotConvert(d, s) + } + ensureLen(d, len(s)) + for i := 0; i < len(s); i++ { + if err := convertAssignValue(d.Index(i), s[i]); err != nil { + return err + } + } + return nil +} + +func convertAssign(d interface{}, s interface{}) (err error) { + if scanner, ok := d.(Scanner); ok { + return scanner.RedisScan(s) + } + + // Handle the most common destination types using type switches and + // fall back to reflection for all other types. + switch s := s.(type) { + case nil: + // ignore + case []byte: + switch d := d.(type) { + case *string: + *d = string(s) + case *int: + *d, err = strconv.Atoi(string(s)) + case *bool: + *d, err = strconv.ParseBool(string(s)) + case *[]byte: + *d = s + case *interface{}: + *d = s + case nil: + // skip value + default: + if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { + err = cannotConvert(d, s) + } else { + err = convertAssignBulkString(d.Elem(), s) + } + } + case int64: + switch d := d.(type) { + case *int: + x := int(s) + if int64(x) != s { + err = strconv.ErrRange + x = 0 + } + *d = x + case *bool: + *d = s != 0 + case *interface{}: + *d = s + case nil: + // skip value + default: + if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { + err = cannotConvert(d, s) + } else { + err = convertAssignInt(d.Elem(), s) + } + } + case string: + switch d := d.(type) { + case *string: + *d = s + case *interface{}: + *d = s + case nil: + // skip value + default: + err = cannotConvert(reflect.ValueOf(d), s) + } + case []interface{}: + switch d := d.(type) { + case *[]interface{}: + *d = s + case *interface{}: + *d = s + case nil: + // skip value + default: + if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { + err = cannotConvert(d, s) + } else { + err = convertAssignArray(d.Elem(), s) + } + } + case Error: + err = s + default: + err = cannotConvert(reflect.ValueOf(d), s) + } + return +} + +// Scan copies from src to the values pointed at by dest. +// +// Scan uses RedisScan if available otherwise: +// +// The values pointed at by dest must be an integer, float, boolean, string, +// []byte, interface{} or slices of these types. Scan uses the standard strconv +// package to convert bulk strings to numeric and boolean types. +// +// If a dest value is nil, then the corresponding src value is skipped. +// +// If a src element is nil, then the corresponding dest value is not modified. +// +// To enable easy use of Scan in a loop, Scan returns the slice of src +// following the copied values. +func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) { + if len(src) < len(dest) { + return nil, errors.New("redigo.Scan: array short") + } + var err error + for i, d := range dest { + err = convertAssign(d, src[i]) + if err != nil { + err = fmt.Errorf("redigo.Scan: cannot assign to dest %d: %v", i, err) + break + } + } + return src[len(dest):], err +} + +type fieldSpec struct { + name string + index []int + omitEmpty bool +} + +type structSpec struct { + m map[string]*fieldSpec + l []*fieldSpec +} + +func (ss *structSpec) fieldSpec(name []byte) *fieldSpec { + return ss.m[string(name)] +} + +func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) { +LOOP: + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + switch { + case f.PkgPath != "" && !f.Anonymous: + // Ignore unexported fields. + case f.Anonymous: + switch f.Type.Kind() { + case reflect.Struct: + compileStructSpec(f.Type, depth, append(index, i), ss) + case reflect.Ptr: + // TODO(steve): Protect against infinite recursion. + if f.Type.Elem().Kind() == reflect.Struct { + compileStructSpec(f.Type.Elem(), depth, append(index, i), ss) + } + } + default: + fs := &fieldSpec{name: f.Name} + tag := f.Tag.Get("redis") + + var ( + p string + ) + first := true + for len(tag) > 0 { + i := strings.IndexByte(tag, ',') + if i < 0 { + p, tag = tag, "" + } else { + p, tag = tag[:i], tag[i+1:] + } + if p == "-" { + continue LOOP + } + if first && len(p) > 0 { + fs.name = p + first = false + } else { + switch p { + case "omitempty": + fs.omitEmpty = true + default: + panic(fmt.Errorf("redigo: unknown field tag %s for type %s", p, t.Name())) + } + } + } + d, found := depth[fs.name] + if !found { + d = 1 << 30 + } + switch { + case len(index) == d: + // At same depth, remove from result. + delete(ss.m, fs.name) + j := 0 + for i := 0; i < len(ss.l); i++ { + if fs.name != ss.l[i].name { + ss.l[j] = ss.l[i] + j += 1 + } + } + ss.l = ss.l[:j] + case len(index) < d: + fs.index = make([]int, len(index)+1) + copy(fs.index, index) + fs.index[len(index)] = i + depth[fs.name] = len(index) + ss.m[fs.name] = fs + ss.l = append(ss.l, fs) + } + } + } +} + +var ( + structSpecMutex sync.RWMutex + structSpecCache = make(map[reflect.Type]*structSpec) +) + +func structSpecForType(t reflect.Type) *structSpec { + + structSpecMutex.RLock() + ss, found := structSpecCache[t] + structSpecMutex.RUnlock() + if found { + return ss + } + + structSpecMutex.Lock() + defer structSpecMutex.Unlock() + ss, found = structSpecCache[t] + if found { + return ss + } + + ss = &structSpec{m: make(map[string]*fieldSpec)} + compileStructSpec(t, make(map[string]int), nil, ss) + structSpecCache[t] = ss + return ss +} + +var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil pointer to a struct") + +// ScanStruct scans alternating names and values from src to a struct. The +// HGETALL and CONFIG GET commands return replies in this format. +// +// ScanStruct uses exported field names to match values in the response. Use +// 'redis' field tag to override the name: +// +// Field int `redis:"myName"` +// +// Fields with the tag redis:"-" are ignored. +// +// Each field uses RedisScan if available otherwise: +// Integer, float, boolean, string and []byte fields are supported. Scan uses the +// standard strconv package to convert bulk string values to numeric and +// boolean types. +// +// If a src element is nil, then the corresponding field is not modified. +func ScanStruct(src []interface{}, dest interface{}) error { + d := reflect.ValueOf(dest) + if d.Kind() != reflect.Ptr || d.IsNil() { + return errScanStructValue + } + d = d.Elem() + if d.Kind() != reflect.Struct { + return errScanStructValue + } + ss := structSpecForType(d.Type()) + + if len(src)%2 != 0 { + return errors.New("redigo.ScanStruct: number of values not a multiple of 2") + } + + for i := 0; i < len(src); i += 2 { + s := src[i+1] + if s == nil { + continue + } + name, ok := src[i].([]byte) + if !ok { + return fmt.Errorf("redigo.ScanStruct: key %d not a bulk string value", i) + } + fs := ss.fieldSpec(name) + if fs == nil { + continue + } + if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil { + return fmt.Errorf("redigo.ScanStruct: cannot assign field %s: %v", fs.name, err) + } + } + return nil +} + +var ( + errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct") +) + +// ScanSlice scans src to the slice pointed to by dest. +// +// If the target is a slice of types which implement Scanner then the custom +// RedisScan method is used otherwise the following rules apply: +// +// The elements in the dest slice must be integer, float, boolean, string, struct +// or pointer to struct values. +// +// Struct fields must be integer, float, boolean or string values. All struct +// fields are used unless a subset is specified using fieldNames. +func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error { + d := reflect.ValueOf(dest) + if d.Kind() != reflect.Ptr || d.IsNil() { + return errScanSliceValue + } + d = d.Elem() + if d.Kind() != reflect.Slice { + return errScanSliceValue + } + + isPtr := false + t := d.Type().Elem() + st := t + if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct { + isPtr = true + t = t.Elem() + } + + if t.Kind() != reflect.Struct || st.Implements(scannerType) { + ensureLen(d, len(src)) + for i, s := range src { + if s == nil { + continue + } + if err := convertAssignValue(d.Index(i), s); err != nil { + return fmt.Errorf("redigo.ScanSlice: cannot assign element %d: %v", i, err) + } + } + return nil + } + + ss := structSpecForType(t) + fss := ss.l + if len(fieldNames) > 0 { + fss = make([]*fieldSpec, len(fieldNames)) + for i, name := range fieldNames { + fss[i] = ss.m[name] + if fss[i] == nil { + return fmt.Errorf("redigo.ScanSlice: ScanSlice bad field name %s", name) + } + } + } + + if len(fss) == 0 { + return errors.New("redigo.ScanSlice: no struct fields") + } + + n := len(src) / len(fss) + if n*len(fss) != len(src) { + return errors.New("redigo.ScanSlice: length not a multiple of struct field count") + } + + ensureLen(d, n) + for i := 0; i < n; i++ { + d := d.Index(i) + if isPtr { + if d.IsNil() { + d.Set(reflect.New(t)) + } + d = d.Elem() + } + for j, fs := range fss { + s := src[i*len(fss)+j] + if s == nil { + continue + } + if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil { + return fmt.Errorf("redigo.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err) + } + } + } + return nil +} + +// Args is a helper for constructing command arguments from structured values. +type Args []interface{} + +// Add returns the result of appending value to args. +func (args Args) Add(value ...interface{}) Args { + return append(args, value...) +} + +// AddFlat returns the result of appending the flattened value of v to args. +// +// Maps are flattened by appending the alternating keys and map values to args. +// +// Slices are flattened by appending the slice elements to args. +// +// Structs are flattened by appending the alternating names and values of +// exported fields to args. If v is a nil struct pointer, then nothing is +// appended. The 'redis' field tag overrides struct field names. See ScanStruct +// for more information on the use of the 'redis' field tag. +// +// Other types are appended to args as is. +func (args Args) AddFlat(v interface{}) Args { + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Struct: + args = flattenStruct(args, rv) + case reflect.Slice: + for i := 0; i < rv.Len(); i++ { + args = append(args, rv.Index(i).Interface()) + } + case reflect.Map: + for _, k := range rv.MapKeys() { + args = append(args, k.Interface(), rv.MapIndex(k).Interface()) + } + case reflect.Ptr: + if rv.Type().Elem().Kind() == reflect.Struct { + if !rv.IsNil() { + args = flattenStruct(args, rv.Elem()) + } + } else { + args = append(args, v) + } + default: + args = append(args, v) + } + return args +} + +func flattenStruct(args Args, v reflect.Value) Args { + ss := structSpecForType(v.Type()) + for _, fs := range ss.l { + fv := v.FieldByIndex(fs.index) + if fs.omitEmpty { + var empty = false + switch fv.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + empty = fv.Len() == 0 + case reflect.Bool: + empty = !fv.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + empty = fv.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + empty = fv.Uint() == 0 + case reflect.Float32, reflect.Float64: + empty = fv.Float() == 0 + case reflect.Interface, reflect.Ptr: + empty = fv.IsNil() + } + if empty { + continue + } + } + if arg, ok := fv.Interface().(Argument); ok { + args = append(args, fs.name, arg.RedisArg()) + } else if fv.Kind() == reflect.Ptr { + if !fv.IsNil() { + args = append(args, fs.name, fv.Elem().Interface()) + } + } else { + args = append(args, fs.name, fv.Interface()) + } + } + return args +} diff --git a/vendor/github.com/gomodule/redigo/redis/script.go b/vendor/github.com/gomodule/redigo/redis/script.go new file mode 100644 index 00000000000..d0cec1ed98e --- /dev/null +++ b/vendor/github.com/gomodule/redigo/redis/script.go @@ -0,0 +1,91 @@ +// Copyright 2012 Gary Burd +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package redis + +import ( + "crypto/sha1" + "encoding/hex" + "io" + "strings" +) + +// Script encapsulates the source, hash and key count for a Lua script. See +// http://redis.io/commands/eval for information on scripts in Redis. +type Script struct { + keyCount int + src string + hash string +} + +// NewScript returns a new script object. If keyCount is greater than or equal +// to zero, then the count is automatically inserted in the EVAL command +// argument list. If keyCount is less than zero, then the application supplies +// the count as the first value in the keysAndArgs argument to the Do, Send and +// SendHash methods. +func NewScript(keyCount int, src string) *Script { + h := sha1.New() + io.WriteString(h, src) // nolint: errcheck + return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))} +} + +func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} { + var args []interface{} + if s.keyCount < 0 { + args = make([]interface{}, 1+len(keysAndArgs)) + args[0] = spec + copy(args[1:], keysAndArgs) + } else { + args = make([]interface{}, 2+len(keysAndArgs)) + args[0] = spec + args[1] = s.keyCount + copy(args[2:], keysAndArgs) + } + return args +} + +// Hash returns the script hash. +func (s *Script) Hash() string { + return s.hash +} + +// Do evaluates the script. Under the covers, Do optimistically evaluates the +// script using the EVALSHA command. If the command fails because the script is +// not loaded, then Do evaluates the script using the EVAL command (thus +// causing the script to load). +func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) { + v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...) + if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") { + v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...) + } + return v, err +} + +// SendHash evaluates the script without waiting for the reply. The script is +// evaluated with the EVALSHA command. The application must ensure that the +// script is loaded by a previous call to Send, Do or Load methods. +func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error { + return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...) +} + +// Send evaluates the script without waiting for the reply. +func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error { + return c.Send("EVAL", s.args(s.src, keysAndArgs)...) +} + +// Load loads the script without evaluating it. +func (s *Script) Load(c Conn) error { + _, err := c.Do("SCRIPT", "LOAD", s.src) + return err +} diff --git a/vendor/github.com/googollee/go-engine.io/.travis.yml b/vendor/github.com/googollee/go-engine.io/.travis.yml deleted file mode 100644 index 0a5134f0dc8..00000000000 --- a/vendor/github.com/googollee/go-engine.io/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: go -go: 1.5 -install: - - go get "github.com/smartystreets/goconvey/convey" - - go get -v . -script: - - go test -race -v ./... diff --git a/vendor/github.com/googollee/go-engine.io/LICENSE b/vendor/github.com/googollee/go-engine.io/LICENSE deleted file mode 100644 index 04a44302163..00000000000 --- a/vendor/github.com/googollee/go-engine.io/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2014-2014 Googol Lee - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. 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. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. \ No newline at end of file diff --git a/vendor/github.com/googollee/go-engine.io/ioutil.go b/vendor/github.com/googollee/go-engine.io/ioutil.go deleted file mode 100644 index 7b933de8dad..00000000000 --- a/vendor/github.com/googollee/go-engine.io/ioutil.go +++ /dev/null @@ -1,50 +0,0 @@ -package engineio - -import ( - "github.com/googollee/go-engine.io/parser" - "io" - "sync" -) - -type connReader struct { - *parser.PacketDecoder - closeChan chan struct{} -} - -func newConnReader(d *parser.PacketDecoder, closeChan chan struct{}) *connReader { - return &connReader{ - PacketDecoder: d, - closeChan: closeChan, - } -} - -func (r *connReader) Close() error { - if r.closeChan == nil { - return nil - } - r.closeChan <- struct{}{} - r.closeChan = nil - return nil -} - -type connWriter struct { - io.WriteCloser - locker *sync.Mutex -} - -func newConnWriter(w io.WriteCloser, locker *sync.Mutex) *connWriter { - return &connWriter{ - WriteCloser: w, - locker: locker, - } -} - -func (w *connWriter) Close() error { - defer func() { - if w.locker != nil { - w.locker.Unlock() - w.locker = nil - } - }() - return w.WriteCloser.Close() -} diff --git a/vendor/github.com/googollee/go-engine.io/message/message.go b/vendor/github.com/googollee/go-engine.io/message/message.go deleted file mode 100644 index 6ff50b64600..00000000000 --- a/vendor/github.com/googollee/go-engine.io/message/message.go +++ /dev/null @@ -1,8 +0,0 @@ -package message - -type MessageType int - -const ( - MessageText MessageType = iota - MessageBinary -) diff --git a/vendor/github.com/googollee/go-engine.io/parser/limit_reader.go b/vendor/github.com/googollee/go-engine.io/parser/limit_reader.go deleted file mode 100644 index f0e1264d3c5..00000000000 --- a/vendor/github.com/googollee/go-engine.io/parser/limit_reader.go +++ /dev/null @@ -1,45 +0,0 @@ -package parser - -import ( - "io" -) - -type limitReader struct { - io.Reader - remain int -} - -func newLimitReader(r io.Reader, limit int) *limitReader { - return &limitReader{ - Reader: r, - remain: limit, - } -} - -func (r *limitReader) Read(b []byte) (int, error) { - if r.remain == 0 { - return 0, io.EOF - } - if len(b) > r.remain { - b = b[:r.remain] - } - n, err := r.Reader.Read(b) - r.remain -= n - return n, err -} - -func (r *limitReader) Close() error { - if r.remain > 0 { - b := make([]byte, 10240) - for { - _, err := r.Read(b) - if err == io.EOF { - break - } - if err != nil { - return err - } - } - } - return nil -} diff --git a/vendor/github.com/googollee/go-engine.io/parser/packet.go b/vendor/github.com/googollee/go-engine.io/parser/packet.go deleted file mode 100644 index be3f78a4f86..00000000000 --- a/vendor/github.com/googollee/go-engine.io/parser/packet.go +++ /dev/null @@ -1,191 +0,0 @@ -package parser - -import ( - "encoding/base64" - "fmt" - "io" - - "github.com/googollee/go-engine.io/message" -) - -// PacketType is the type of packet -type PacketType string - -const ( - OPEN PacketType = "open" - CLOSE PacketType = "close" - PING PacketType = "ping" - PONG PacketType = "pong" - MESSAGE PacketType = "message" - UPGRADE PacketType = "upgrade" - NOOP PacketType = "noop" -) - -func ByteToType(b byte) (PacketType, error) { - switch b { - case 0: - return OPEN, nil - case 1: - return CLOSE, nil - case 2: - return PING, nil - case 3: - return PONG, nil - case 4: - return MESSAGE, nil - case 5: - return UPGRADE, nil - case 6: - return NOOP, nil - } - return NOOP, fmt.Errorf("invalid byte 0x%x", b) -} - -// Byte return the byte of type -func (t PacketType) Byte() byte { - switch t { - case OPEN: - return 0 - case CLOSE: - return 1 - case PING: - return 2 - case PONG: - return 3 - case MESSAGE: - return 4 - case UPGRADE: - return 5 - } - return 6 -} - -// packetEncoder is the encoder which encode the packet. -type PacketEncoder struct { - closer io.Closer - w io.Writer -} - -// NewStringEncoder return the encoder which encode type t to writer w, as string. -func NewStringEncoder(w io.Writer, t PacketType) (*PacketEncoder, error) { - return newEncoder(w, t.Byte()+'0') -} - -// NewBinaryEncoder return the encoder which encode type t to writer w, as binary. -func NewBinaryEncoder(w io.Writer, t PacketType) (*PacketEncoder, error) { - return newEncoder(w, t.Byte()) -} - -func newEncoder(w io.Writer, t byte) (*PacketEncoder, error) { - if _, err := w.Write([]byte{t}); err != nil { - return nil, err - } - closer, ok := w.(io.Closer) - if !ok { - closer = nil - } - return &PacketEncoder{ - closer: closer, - w: w, - }, nil -} - -// NewB64Encoder return the encoder which encode type t to writer w, as string. When write binary, it uses base64. -func NewB64Encoder(w io.Writer, t PacketType) (*PacketEncoder, error) { - _, err := w.Write([]byte{'b', t.Byte() + '0'}) - if err != nil { - return nil, err - } - base := base64.NewEncoder(base64.StdEncoding, w) - return &PacketEncoder{ - closer: base, - w: base, - }, nil -} - -// Write writes bytes p. -func (e *PacketEncoder) Write(p []byte) (int, error) { - return e.w.Write(p) -} - -// Close closes the encoder. -func (e *PacketEncoder) Close() error { - if e.closer != nil { - return e.closer.Close() - } - return nil -} - -// packetDecoder is the decoder which decode data to packet. -type PacketDecoder struct { - closer io.Closer - r io.Reader - t PacketType - msgType message.MessageType -} - -// NewDecoder return the decoder which decode from reader r. -func NewDecoder(r io.Reader) (*PacketDecoder, error) { - var closer io.Closer - if limit, ok := r.(*limitReader); ok { - closer = limit - } - defer func() { - if closer != nil { - closer.Close() - } - }() - - b := []byte{0xff} - if _, err := r.Read(b); err != nil { - return nil, err - } - msgType := message.MessageText - if b[0] == 'b' { - if _, err := r.Read(b); err != nil { - return nil, err - } - r = base64.NewDecoder(base64.StdEncoding, r) - msgType = message.MessageBinary - } - if b[0] >= '0' { - b[0] = b[0] - '0' - } else { - msgType = message.MessageBinary - } - t, err := ByteToType(b[0]) - if err != nil { - return nil, err - } - ret := &PacketDecoder{ - closer: closer, - r: r, - t: t, - msgType: msgType, - } - closer = nil - return ret, nil -} - -// Read reads packet data to bytes p. -func (d *PacketDecoder) Read(p []byte) (int, error) { - return d.r.Read(p) -} - -// Type returns the type of packet. -func (d *PacketDecoder) Type() PacketType { - return d.t -} - -// MessageType returns the type of message, binary or string. -func (d *PacketDecoder) MessageType() message.MessageType { - return d.msgType -} - -// Close closes the decoder. -func (d *PacketDecoder) Close() error { - if d.closer != nil { - return d.closer.Close() - } - return nil -} diff --git a/vendor/github.com/googollee/go-engine.io/parser/parser.go b/vendor/github.com/googollee/go-engine.io/parser/parser.go deleted file mode 100644 index b55076248f5..00000000000 --- a/vendor/github.com/googollee/go-engine.io/parser/parser.go +++ /dev/null @@ -1,3 +0,0 @@ -package parser - -const Protocol = 3 diff --git a/vendor/github.com/googollee/go-engine.io/parser/payload.go b/vendor/github.com/googollee/go-engine.io/parser/payload.go deleted file mode 100644 index 41fd1c6ced6..00000000000 --- a/vendor/github.com/googollee/go-engine.io/parser/payload.go +++ /dev/null @@ -1,170 +0,0 @@ -package parser - -import ( - "bufio" - "bytes" - "fmt" - "io" - "strconv" - "sync" -) - -// payloadEncoder is the encoder to encode packets as payload. It can be used in multi-thread. -type PayloadEncoder struct { - buffers [][]byte - locker sync.Mutex - isString bool -} - -// NewStringPayloadEncoder returns the encoder which encode as string. -func NewStringPayloadEncoder() *PayloadEncoder { - return &PayloadEncoder{ - isString: true, - } -} - -// NewStringPayloadEncoder returns the encoder which encode as binary. -func NewBinaryPayloadEncoder() *PayloadEncoder { - return &PayloadEncoder{ - isString: false, - } -} - -type encoder struct { - *PacketEncoder - buf *bytes.Buffer - binaryPrefix string - payload *PayloadEncoder -} - -func (e encoder) Close() error { - if err := e.PacketEncoder.Close(); err != nil { - return err - } - var buffer []byte - if e.payload.isString { - buffer = []byte(fmt.Sprintf("%d:%s", e.buf.Len(), e.buf.String())) - } else { - buffer = []byte(fmt.Sprintf("%s%d", e.binaryPrefix, e.buf.Len())) - for i, n := 0, len(buffer); i < n; i++ { - buffer[i] = buffer[i] - '0' - } - buffer = append(buffer, 0xff) - buffer = append(buffer, e.buf.Bytes()...) - } - - e.payload.locker.Lock() - e.payload.buffers = append(e.payload.buffers, buffer) - e.payload.locker.Unlock() - - return nil -} - -// NextString returns the encoder with packet type t and encode as string. -func (e *PayloadEncoder) NextString(t PacketType) (io.WriteCloser, error) { - buf := bytes.NewBuffer(nil) - pEncoder, err := NewStringEncoder(buf, t) - if err != nil { - return nil, err - } - return encoder{ - PacketEncoder: pEncoder, - buf: buf, - binaryPrefix: "0", - payload: e, - }, nil -} - -// NextBinary returns the encoder with packet type t and encode as binary. -func (e *PayloadEncoder) NextBinary(t PacketType) (io.WriteCloser, error) { - buf := bytes.NewBuffer(nil) - var pEncoder *PacketEncoder - var err error - if e.isString { - pEncoder, err = NewB64Encoder(buf, t) - } else { - pEncoder, err = NewBinaryEncoder(buf, t) - } - if err != nil { - return nil, err - } - return encoder{ - PacketEncoder: pEncoder, - buf: buf, - binaryPrefix: "1", - payload: e, - }, nil -} - -// EncodeTo writes encoded payload to writer w. It will clear the buffer of encoder. -func (e *PayloadEncoder) EncodeTo(w io.Writer) error { - e.locker.Lock() - buffers := e.buffers - e.buffers = nil - e.locker.Unlock() - - for _, b := range buffers { - for len(b) > 0 { - n, err := w.Write(b) - if err != nil { - return err - } - b = b[n:] - } - } - return nil -} - -//IsString returns true if payload encode to string, otherwise returns false. -func (e *PayloadEncoder) IsString() bool { - return e.isString -} - -// payloadDecoder is the decoder to decode payload. -type PayloadDecoder struct { - r *bufio.Reader -} - -// NewPaylaodDecoder returns the payload decoder which read from reader r. -func NewPayloadDecoder(r io.Reader) *PayloadDecoder { - br, ok := r.(*bufio.Reader) - if !ok { - br = bufio.NewReader(r) - } - return &PayloadDecoder{ - r: br, - } -} - -// Next returns the packet decoder. Make sure it will be closed after used. -func (d *PayloadDecoder) Next() (*PacketDecoder, error) { - firstByte, err := d.r.Peek(1) - if err != nil { - return nil, err - } - isBinary := firstByte[0] < '0' - delim := byte(':') - if isBinary { - d.r.ReadByte() - delim = 0xff - } - line, err := d.r.ReadBytes(delim) - if err != nil { - return nil, err - } - l := len(line) - if l < 1 { - return nil, fmt.Errorf("invalid input") - } - lenByte := line[:l-1] - if isBinary { - for i, n := 0, l; i < n; i++ { - line[i] = line[i] + '0' - } - } - packetLen, err := strconv.ParseInt(string(lenByte), 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid input") - } - return NewDecoder(newLimitReader(d.r, int(packetLen))) -} diff --git a/vendor/github.com/googollee/go-engine.io/polling/client.go b/vendor/github.com/googollee/go-engine.io/polling/client.go deleted file mode 100644 index 842a715d8a0..00000000000 --- a/vendor/github.com/googollee/go-engine.io/polling/client.go +++ /dev/null @@ -1,149 +0,0 @@ -package polling - -import ( - "bytes" - "fmt" - "github.com/googollee/go-engine.io/message" - "io" - "io/ioutil" - "net/http" - "net/url" - "time" - - "github.com/googollee/go-engine.io/parser" - "github.com/googollee/go-engine.io/transport" -) - -type client struct { - req http.Request - url url.URL - seq uint - getResp *http.Response - postResp *http.Response - resp *http.Response - payloadDecoder *parser.PayloadDecoder - payloadEncoder *parser.PayloadEncoder - client *http.Client - state state -} - -func NewClient(r *http.Request) (transport.Client, error) { - newEncoder := parser.NewBinaryPayloadEncoder - if _, ok := r.URL.Query()["b64"]; ok { - newEncoder = parser.NewStringPayloadEncoder - } - ret := &client{ - req: *r, - url: *r.URL, - seq: 0, - payloadEncoder: newEncoder(), - client: http.DefaultClient, - state: stateNormal, - } - return ret, nil -} - -func (c *client) Response() *http.Response { - return c.resp -} - -func (c *client) NextReader() (*parser.PacketDecoder, error) { - if c.state != stateNormal { - return nil, io.EOF - } - if c.payloadDecoder != nil { - ret, err := c.payloadDecoder.Next() - if err != io.EOF { - return ret, err - } - c.getResp.Body.Close() - c.payloadDecoder = nil - } - req := c.getReq() - req.Method = "GET" - var err error - c.getResp, err = c.client.Do(req) - if err != nil { - return nil, err - } - if c.resp == nil { - c.resp = c.getResp - } - c.payloadDecoder = parser.NewPayloadDecoder(c.getResp.Body) - return c.payloadDecoder.Next() -} - -func (c *client) NextWriter(messageType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error) { - if c.state != stateNormal { - return nil, io.EOF - } - next := c.payloadEncoder.NextBinary - if messageType == message.MessageText { - next = c.payloadEncoder.NextString - } - w, err := next(packetType) - if err != nil { - return nil, err - } - return newClientWriter(c, w), nil -} - -func (c *client) Close() error { - if c.state != stateNormal { - return nil - } - c.state = stateClosed - return nil -} - -func (c *client) getReq() *http.Request { - req := c.req - url := c.url - req.URL = &url - query := req.URL.Query() - query.Set("t", fmt.Sprintf("%d-%d", time.Now().Unix()*1000, c.seq)) - c.seq++ - req.URL.RawQuery = query.Encode() - return &req -} - -func (c *client) doPost() error { - if c.state != stateNormal { - return io.EOF - } - req := c.getReq() - req.Method = "POST" - buf := bytes.NewBuffer(nil) - if err := c.payloadEncoder.EncodeTo(buf); err != nil { - return err - } - req.Body = ioutil.NopCloser(buf) - var err error - c.postResp, err = c.client.Do(req) - if err != nil { - return err - } - if c.resp == nil { - c.resp = c.postResp - } - return nil -} - -type clientWriter struct { - io.WriteCloser - client *client -} - -func newClientWriter(c *client, w io.WriteCloser) io.WriteCloser { - return &clientWriter{ - WriteCloser: w, - client: c, - } -} - -func (w *clientWriter) Close() error { - if err := w.WriteCloser.Close(); err != nil { - return err - } - return w.client.doPost() -} diff --git a/vendor/github.com/googollee/go-engine.io/polling/server.go b/vendor/github.com/googollee/go-engine.io/polling/server.go deleted file mode 100644 index 7de1acbc2f6..00000000000 --- a/vendor/github.com/googollee/go-engine.io/polling/server.go +++ /dev/null @@ -1,197 +0,0 @@ -package polling - -import ( - "bytes" - "html/template" - "io" - "net/http" - "sync" - - "github.com/googollee/go-engine.io/message" - "github.com/googollee/go-engine.io/parser" - "github.com/googollee/go-engine.io/transport" -) - -type state int - -const ( - stateUnknow state = iota - stateNormal - stateClosing - stateClosed -) - -type Polling struct { - sendChan chan bool - encoder *parser.PayloadEncoder - callback transport.Callback - getLocker *Locker - postLocker *Locker - state state - stateLocker sync.Mutex -} - -func NewServer(w http.ResponseWriter, r *http.Request, callback transport.Callback) (transport.Server, error) { - newEncoder := parser.NewBinaryPayloadEncoder - if r.URL.Query()["b64"] != nil { - newEncoder = parser.NewStringPayloadEncoder - } - ret := &Polling{ - sendChan: MakeSendChan(), - encoder: newEncoder(), - callback: callback, - getLocker: NewLocker(), - postLocker: NewLocker(), - state: stateNormal, - } - return ret, nil -} - -func (p *Polling) ServeHTTP(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - p.get(w, r) - case "POST": - p.post(w, r) - } -} - -func (p *Polling) Close() error { - if p.getState() != stateNormal { - return nil - } - p.setState(stateClosing) - close(p.sendChan) - if p.getLocker.TryLock() { - if p.postLocker.TryLock() { - p.callback.OnClose(p) - p.setState(stateClosed) - p.postLocker.Unlock() - } - p.getLocker.Unlock() - } - return nil -} - -func (p *Polling) NextWriter(msgType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error) { - if p.getState() != stateNormal { - return nil, io.EOF - } - - var ret io.WriteCloser - var err error - switch msgType { - case message.MessageText: - ret, err = p.encoder.NextString(packetType) - case message.MessageBinary: - ret, err = p.encoder.NextBinary(packetType) - } - - if err != nil { - return nil, err - } - return NewWriter(ret, p), nil -} - -func (p *Polling) get(w http.ResponseWriter, r *http.Request) { - if !p.getLocker.TryLock() { - http.Error(w, "overlay get", http.StatusBadRequest) - return - } - if p.getState() != stateNormal { - http.Error(w, "closed", http.StatusBadRequest) - return - } - - defer func() { - if p.getState() == stateClosing { - if p.postLocker.TryLock() { - p.setState(stateClosed) - p.callback.OnClose(p) - p.postLocker.Unlock() - } - } - p.getLocker.Unlock() - }() - - <-p.sendChan - - if j := r.URL.Query().Get("j"); j != "" { - // JSONP Polling - w.Header().Set("Content-Type", "text/javascript; charset=UTF-8") - tmp := bytes.Buffer{} - p.encoder.EncodeTo(&tmp) - pl := template.JSEscapeString(tmp.String()) - w.Write([]byte("___eio[" + j + "](\"")) - w.Write([]byte(pl)) - w.Write([]byte("\");")) - } else { - // XHR Polling - if p.encoder.IsString() { - w.Header().Set("Content-Type", "text/plain; charset=UTF-8") - } else { - w.Header().Set("Content-Type", "application/octet-stream") - } - p.encoder.EncodeTo(w) - } - -} - -func (p *Polling) post(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html") - if !p.postLocker.TryLock() { - http.Error(w, "overlay post", http.StatusBadRequest) - return - } - if p.getState() != stateNormal { - http.Error(w, "closed", http.StatusBadRequest) - return - } - - defer func() { - if p.getState() == stateClosing { - if p.getLocker.TryLock() { - p.setState(stateClosed) - p.callback.OnClose(p) - p.getLocker.Unlock() - } - } - p.postLocker.Unlock() - }() - - var decoder *parser.PayloadDecoder - if j := r.URL.Query().Get("j"); j != "" { - // JSONP Polling - d := r.FormValue("d") - decoder = parser.NewPayloadDecoder(bytes.NewBufferString(d)) - } else { - // XHR Polling - decoder = parser.NewPayloadDecoder(r.Body) - } - for { - d, err := decoder.Next() - if err == io.EOF { - break - } - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - p.callback.OnPacket(d) - d.Close() - } - w.Write([]byte("ok")) -} - -func (p *Polling) setState(s state) { - p.stateLocker.Lock() - defer p.stateLocker.Unlock() - p.state = s -} - -func (p *Polling) getState() state { - p.stateLocker.Lock() - defer p.stateLocker.Unlock() - return p.state -} diff --git a/vendor/github.com/googollee/go-engine.io/polling/try_locker.go b/vendor/github.com/googollee/go-engine.io/polling/try_locker.go deleted file mode 100644 index 28f0ad3851e..00000000000 --- a/vendor/github.com/googollee/go-engine.io/polling/try_locker.go +++ /dev/null @@ -1,28 +0,0 @@ -package polling - -type Locker struct { - locker chan struct{} -} - -func NewLocker() *Locker { - return &Locker{ - locker: make(chan struct{}, 1), - } -} - -func (l *Locker) Lock() { - l.locker <- struct{}{} -} - -func (l *Locker) TryLock() bool { - select { - case l.locker <- struct{}{}: - return true - default: - return false - } -} - -func (l *Locker) Unlock() { - <-l.locker -} diff --git a/vendor/github.com/googollee/go-engine.io/polling/writer.go b/vendor/github.com/googollee/go-engine.io/polling/writer.go deleted file mode 100644 index 2c81b5c3393..00000000000 --- a/vendor/github.com/googollee/go-engine.io/polling/writer.go +++ /dev/null @@ -1,33 +0,0 @@ -package polling - -import ( - "errors" - "io" -) - -func MakeSendChan() chan bool { - return make(chan bool, 1) -} - -type Writer struct { - io.WriteCloser - server *Polling -} - -func NewWriter(w io.WriteCloser, server *Polling) *Writer { - return &Writer{ - WriteCloser: w, - server: server, - } -} - -func (w *Writer) Close() error { - if w.server.getState() != stateNormal { - return errors.New("use of closed network connection") - } - select { - case w.server.sendChan <- true: - default: - } - return w.WriteCloser.Close() -} diff --git a/vendor/github.com/googollee/go-engine.io/polling/xhr.go b/vendor/github.com/googollee/go-engine.io/polling/xhr.go deleted file mode 100644 index d86a51ff4c1..00000000000 --- a/vendor/github.com/googollee/go-engine.io/polling/xhr.go +++ /dev/null @@ -1,12 +0,0 @@ -package polling - -import ( - "github.com/googollee/go-engine.io/transport" -) - -var Creater = transport.Creater{ - Name: "polling", - Upgrading: false, - Server: NewServer, - Client: NewClient, -} diff --git a/vendor/github.com/googollee/go-engine.io/server.go b/vendor/github.com/googollee/go-engine.io/server.go deleted file mode 100644 index 24a14206db6..00000000000 --- a/vendor/github.com/googollee/go-engine.io/server.go +++ /dev/null @@ -1,189 +0,0 @@ -package engineio - -import ( - "bytes" - "crypto/md5" - "encoding/base64" - "fmt" - "net/http" - "sync/atomic" - "time" - - "github.com/googollee/go-engine.io/polling" - "github.com/googollee/go-engine.io/websocket" -) - -type config struct { - PingTimeout time.Duration - PingInterval time.Duration - MaxConnection int - AllowRequest func(*http.Request) error - AllowUpgrades bool - Cookie string - NewId func(r *http.Request) string -} - -// Server is the server of engine.io. -type Server struct { - config config - socketChan chan Conn - serverSessions Sessions - creaters transportCreaters - currentConnection int32 -} - -// NewServer returns the server suppported given transports. If transports is nil, server will use ["polling", "websocket"] as default. -func NewServer(transports []string) (*Server, error) { - if transports == nil { - transports = []string{"polling", "websocket"} - } - creaters := make(transportCreaters) - for _, t := range transports { - switch t { - case "polling": - creaters[t] = polling.Creater - case "websocket": - creaters[t] = websocket.Creater - default: - return nil, InvalidError - } - } - return &Server{ - config: config{ - PingTimeout: 60000 * time.Millisecond, - PingInterval: 25000 * time.Millisecond, - MaxConnection: 1000, - AllowRequest: func(*http.Request) error { return nil }, - AllowUpgrades: true, - Cookie: "io", - NewId: newId, - }, - socketChan: make(chan Conn), - serverSessions: newServerSessions(), - creaters: creaters, - }, nil -} - -// SetPingTimeout sets the timeout of ping. When time out, server will close connection. Default is 60s. -func (s *Server) SetPingTimeout(t time.Duration) { - s.config.PingTimeout = t -} - -// SetPingInterval sets the interval of ping. Default is 25s. -func (s *Server) SetPingInterval(t time.Duration) { - s.config.PingInterval = t -} - -// SetMaxConnection sets the max connetion. Default is 1000. -func (s *Server) SetMaxConnection(n int) { - s.config.MaxConnection = n -} - -// GetMaxConnection returns the current max connection -func (s *Server) GetMaxConnection() int { - return s.config.MaxConnection -} - -// Count returns a count of current number of active connections in session -func (s *Server) Count() int { - return int(atomic.LoadInt32(&s.currentConnection)) -} - -// SetAllowRequest sets the middleware function when establish connection. If it return non-nil, connection won't be established. Default will allow all request. -func (s *Server) SetAllowRequest(f func(*http.Request) error) { - s.config.AllowRequest = f -} - -// SetAllowUpgrades sets whether server allows transport upgrade. Default is true. -func (s *Server) SetAllowUpgrades(allow bool) { - s.config.AllowUpgrades = allow -} - -// SetCookie sets the name of cookie which used by engine.io. Default is "io". -func (s *Server) SetCookie(prefix string) { - s.config.Cookie = prefix -} - -// SetNewId sets the callback func to generate new connection id. By default, id is generated from remote addr + current time stamp -func (s *Server) SetNewId(f func(*http.Request) string) { - s.config.NewId = f -} - -// SetSessionManager sets the sessions as server's session manager. Default sessions is single process manager. You can custom it as load balance. -func (s *Server) SetSessionManager(sessions Sessions) { - s.serverSessions = sessions -} - -// ServeHTTP handles http request. -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - - sid := r.URL.Query().Get("sid") - conn := s.serverSessions.Get(sid) - if conn == nil { - if sid != "" { - http.Error(w, "invalid sid", http.StatusBadRequest) - return - } - - if err := s.config.AllowRequest(r); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - n := atomic.AddInt32(&s.currentConnection, 1) - if int(n) > s.config.MaxConnection { - atomic.AddInt32(&s.currentConnection, -1) - http.Error(w, "too many connections", http.StatusServiceUnavailable) - return - } - - sid = s.config.NewId(r) - - var err error - conn, err = newServerConn(sid, w, r, s) - if err != nil { - atomic.AddInt32(&s.currentConnection, -1) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - s.serverSessions.Set(sid, conn) - - s.socketChan <- conn - } - http.SetCookie(w, &http.Cookie{ - Name: s.config.Cookie, - Value: sid, - }) - - conn.(*serverConn).ServeHTTP(w, r) -} - -// Accept returns Conn when client connect to server. -func (s *Server) Accept() (Conn, error) { - return <-s.socketChan, nil -} - -func (s *Server) configure() config { - return s.config -} - -func (s *Server) transports() transportCreaters { - return s.creaters -} - -func (s *Server) onClose(id string) { - s.serverSessions.Remove(id) - atomic.AddInt32(&s.currentConnection, -1) -} - -func newId(r *http.Request) string { - hash := fmt.Sprintf("%s %s", r.RemoteAddr, time.Now()) - buf := bytes.NewBuffer(nil) - sum := md5.Sum([]byte(hash)) - encoder := base64.NewEncoder(base64.URLEncoding, buf) - encoder.Write(sum[:]) - encoder.Close() - return buf.String()[:20] -} diff --git a/vendor/github.com/googollee/go-engine.io/server_conn.go b/vendor/github.com/googollee/go-engine.io/server_conn.go deleted file mode 100644 index ae85459fd1b..00000000000 --- a/vendor/github.com/googollee/go-engine.io/server_conn.go +++ /dev/null @@ -1,388 +0,0 @@ -package engineio - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "sync" - "time" - - "github.com/googollee/go-engine.io/message" - "github.com/googollee/go-engine.io/parser" - "github.com/googollee/go-engine.io/transport" -) - -type MessageType message.MessageType - -const ( - MessageBinary MessageType = MessageType(message.MessageBinary) - MessageText MessageType = MessageType(message.MessageText) -) - -// Conn is the connection object of engine.io. -type Conn interface { - - // Id returns the session id of connection. - Id() string - - // Request returns the first http request when established connection. - Request() *http.Request - - // Close closes the connection. - Close() error - - // NextReader returns the next message type, reader. If no message received, it will block. - NextReader() (MessageType, io.ReadCloser, error) - - // NextWriter returns the next message writer with given message type. - NextWriter(messageType MessageType) (io.WriteCloser, error) -} - -type transportCreaters map[string]transport.Creater - -func (c transportCreaters) Get(name string) transport.Creater { - return c[name] -} - -type serverCallback interface { - configure() config - transports() transportCreaters - onClose(sid string) -} - -type state int - -const ( - stateUnknow state = iota - stateNormal - stateUpgrading - stateClosing - stateClosed -) - -type serverConn struct { - id string - request *http.Request - callback serverCallback - writerLocker sync.Mutex - transportLocker sync.RWMutex - currentName string - current transport.Server - upgradingName string - upgrading transport.Server - state state - stateLocker sync.RWMutex - readerChan chan *connReader - pingTimeout time.Duration - pingInterval time.Duration - pingChan chan bool - pingLocker sync.Mutex -} - -var InvalidError = errors.New("invalid transport") - -func newServerConn(id string, w http.ResponseWriter, r *http.Request, callback serverCallback) (*serverConn, error) { - transportName := r.URL.Query().Get("transport") - creater := callback.transports().Get(transportName) - if creater.Name == "" { - return nil, InvalidError - } - ret := &serverConn{ - id: id, - request: r, - callback: callback, - state: stateNormal, - readerChan: make(chan *connReader), - pingTimeout: callback.configure().PingTimeout, - pingInterval: callback.configure().PingInterval, - pingChan: make(chan bool), - } - transport, err := creater.Server(w, r, ret) - if err != nil { - return nil, err - } - ret.setCurrent(transportName, transport) - if err := ret.onOpen(); err != nil { - return nil, err - } - - go ret.pingLoop() - - return ret, nil -} - -func (c *serverConn) Id() string { - return c.id -} - -func (c *serverConn) Request() *http.Request { - return c.request -} - -func (c *serverConn) NextReader() (MessageType, io.ReadCloser, error) { - if c.getState() == stateClosed { - return MessageBinary, nil, io.EOF - } - ret := <-c.readerChan - if ret == nil { - return MessageBinary, nil, io.EOF - } - return MessageType(ret.MessageType()), ret, nil -} - -func (c *serverConn) NextWriter(t MessageType) (io.WriteCloser, error) { - switch c.getState() { - case stateUpgrading: - for i := 0; i < 30; i++ { - time.Sleep(50 * time.Millisecond) - if c.getState() != stateUpgrading { - break - } - } - if c.getState() == stateUpgrading { - return nil, fmt.Errorf("upgrading") - } - case stateNormal: - default: - return nil, io.EOF - } - c.writerLocker.Lock() - ret, err := c.getCurrent().NextWriter(message.MessageType(t), parser.MESSAGE) - if err != nil { - c.writerLocker.Unlock() - return ret, err - } - writer := newConnWriter(ret, &c.writerLocker) - return writer, err -} - -func (c *serverConn) Close() error { - if c.getState() != stateNormal && c.getState() != stateUpgrading { - return nil - } - if c.upgrading != nil { - c.upgrading.Close() - } - c.writerLocker.Lock() - if w, err := c.getCurrent().NextWriter(message.MessageText, parser.CLOSE); err == nil { - writer := newConnWriter(w, &c.writerLocker) - writer.Close() - } else { - c.writerLocker.Unlock() - } - if err := c.getCurrent().Close(); err != nil { - return err - } - c.setState(stateClosing) - return nil -} - -func (c *serverConn) ServeHTTP(w http.ResponseWriter, r *http.Request) { - transportName := r.URL.Query().Get("transport") - if c.currentName != transportName { - creater := c.callback.transports().Get(transportName) - if creater.Name == "" { - http.Error(w, fmt.Sprintf("invalid transport %s", transportName), http.StatusBadRequest) - return - } - u, err := creater.Server(w, r, c) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - c.setUpgrading(creater.Name, u) - return - } - c.current.ServeHTTP(w, r) -} - -func (c *serverConn) OnPacket(r *parser.PacketDecoder) { - if s := c.getState(); s != stateNormal && s != stateUpgrading { - return - } - switch r.Type() { - case parser.OPEN: - case parser.CLOSE: - c.getCurrent().Close() - case parser.PING: - c.writerLocker.Lock() - t := c.getCurrent() - u := c.getUpgrade() - newWriter := t.NextWriter - if u != nil { - if w, _ := t.NextWriter(message.MessageText, parser.NOOP); w != nil { - w.Close() - } - newWriter = u.NextWriter - } - if w, _ := newWriter(message.MessageText, parser.PONG); w != nil { - io.Copy(w, r) - w.Close() - } - c.writerLocker.Unlock() - fallthrough - case parser.PONG: - c.pingLocker.Lock() - defer c.pingLocker.Unlock() - if s := c.getState(); s != stateNormal && s != stateUpgrading { - return - } - c.pingChan <- true - case parser.MESSAGE: - closeChan := make(chan struct{}) - c.readerChan <- newConnReader(r, closeChan) - <-closeChan - close(closeChan) - r.Close() - case parser.UPGRADE: - c.upgraded() - case parser.NOOP: - } -} - -func (c *serverConn) OnClose(server transport.Server) { - if t := c.getUpgrade(); server == t { - c.setUpgrading("", nil) - t.Close() - return - } - t := c.getCurrent() - if server != t { - return - } - t.Close() - if t := c.getUpgrade(); t != nil { - t.Close() - c.setUpgrading("", nil) - } - c.setState(stateClosed) - close(c.readerChan) - c.pingLocker.Lock() - close(c.pingChan) - c.pingLocker.Unlock() - c.callback.onClose(c.id) -} - -func (s *serverConn) onOpen() error { - upgrades := []string{} - for name := range s.callback.transports() { - if name == s.currentName { - continue - } - upgrades = append(upgrades, name) - } - type connectionInfo struct { - Sid string `json:"sid"` - Upgrades []string `json:"upgrades"` - PingInterval time.Duration `json:"pingInterval"` - PingTimeout time.Duration `json:"pingTimeout"` - } - resp := connectionInfo{ - Sid: s.Id(), - Upgrades: upgrades, - PingInterval: s.callback.configure().PingInterval / time.Millisecond, - PingTimeout: s.callback.configure().PingTimeout / time.Millisecond, - } - w, err := s.getCurrent().NextWriter(message.MessageText, parser.OPEN) - if err != nil { - return err - } - encoder := json.NewEncoder(w) - if err := encoder.Encode(resp); err != nil { - return err - } - if err := w.Close(); err != nil { - return err - } - return nil -} - -func (c *serverConn) getCurrent() transport.Server { - c.transportLocker.RLock() - defer c.transportLocker.RUnlock() - - return c.current -} - -func (c *serverConn) getUpgrade() transport.Server { - c.transportLocker.RLock() - defer c.transportLocker.RUnlock() - - return c.upgrading -} - -func (c *serverConn) setCurrent(name string, s transport.Server) { - c.transportLocker.Lock() - defer c.transportLocker.Unlock() - - c.currentName = name - c.current = s -} - -func (c *serverConn) setUpgrading(name string, s transport.Server) { - c.transportLocker.Lock() - defer c.transportLocker.Unlock() - - c.upgradingName = name - c.upgrading = s - c.setState(stateUpgrading) -} - -func (c *serverConn) upgraded() { - c.transportLocker.Lock() - - current := c.current - c.current = c.upgrading - c.currentName = c.upgradingName - c.upgrading = nil - c.upgradingName = "" - - c.transportLocker.Unlock() - - current.Close() - c.setState(stateNormal) -} - -func (c *serverConn) getState() state { - c.stateLocker.RLock() - defer c.stateLocker.RUnlock() - return c.state -} - -func (c *serverConn) setState(state state) { - c.stateLocker.Lock() - defer c.stateLocker.Unlock() - c.state = state -} - -func (c *serverConn) pingLoop() { - lastPing := time.Now() - lastTry := lastPing - for { - now := time.Now() - pingDiff := now.Sub(lastPing) - tryDiff := now.Sub(lastTry) - select { - case ok := <-c.pingChan: - if !ok { - return - } - lastPing = time.Now() - lastTry = lastPing - case <-time.After(c.pingInterval - tryDiff): - c.writerLocker.Lock() - if w, _ := c.getCurrent().NextWriter(message.MessageText, parser.PING); w != nil { - writer := newConnWriter(w, &c.writerLocker) - writer.Close() - } else { - c.writerLocker.Unlock() - } - lastTry = time.Now() - case <-time.After(c.pingTimeout - pingDiff): - c.Close() - return - } - } -} diff --git a/vendor/github.com/googollee/go-engine.io/sessions.go b/vendor/github.com/googollee/go-engine.io/sessions.go deleted file mode 100644 index 6b1daa9645e..00000000000 --- a/vendor/github.com/googollee/go-engine.io/sessions.go +++ /dev/null @@ -1,47 +0,0 @@ -package engineio - -import ( - "sync" -) - -type Sessions interface { - Get(id string) Conn - Set(id string, conn Conn) - Remove(id string) -} - -type serverSessions struct { - sessions map[string]Conn - locker sync.RWMutex -} - -func newServerSessions() *serverSessions { - return &serverSessions{ - sessions: make(map[string]Conn), - } -} - -func (s *serverSessions) Get(id string) Conn { - s.locker.RLock() - defer s.locker.RUnlock() - - ret, ok := s.sessions[id] - if !ok { - return nil - } - return ret -} - -func (s *serverSessions) Set(id string, conn Conn) { - s.locker.Lock() - defer s.locker.Unlock() - - s.sessions[id] = conn -} - -func (s *serverSessions) Remove(id string) { - s.locker.Lock() - defer s.locker.Unlock() - - delete(s.sessions, id) -} diff --git a/vendor/github.com/googollee/go-engine.io/transport/transport.go b/vendor/github.com/googollee/go-engine.io/transport/transport.go deleted file mode 100644 index e549a3082e6..00000000000 --- a/vendor/github.com/googollee/go-engine.io/transport/transport.go +++ /dev/null @@ -1,50 +0,0 @@ -package transport - -import ( - "io" - "net/http" - - "github.com/googollee/go-engine.io/message" - "github.com/googollee/go-engine.io/parser" -) - -type Callback interface { - OnPacket(r *parser.PacketDecoder) - OnClose(server Server) -} - -type Creater struct { - Name string - Upgrading bool - Server func(w http.ResponseWriter, r *http.Request, callback Callback) (Server, error) - Client func(r *http.Request) (Client, error) -} - -// Server is a transport layer in server to connect client. -type Server interface { - - // ServeHTTP handles the http request. It will call conn.onPacket when receive packet. - ServeHTTP(http.ResponseWriter, *http.Request) - - // Close closes the transport. - Close() error - - // NextWriter returns packet writer. This function call should be synced. - NextWriter(messageType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error) -} - -// Client is a transport layer in client to connect server. -type Client interface { - - // Response returns the response of last http request. - Response() *http.Response - - // NextReader returns packet decoder. This function call should be synced. - NextReader() (*parser.PacketDecoder, error) - - // NextWriter returns packet writer. This function call should be synced. - NextWriter(messageType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error) - - // Close closes the transport. - Close() error -} diff --git a/vendor/github.com/googollee/go-engine.io/websocket/client.go b/vendor/github.com/googollee/go-engine.io/websocket/client.go deleted file mode 100644 index eeb2cc27b99..00000000000 --- a/vendor/github.com/googollee/go-engine.io/websocket/client.go +++ /dev/null @@ -1,72 +0,0 @@ -package websocket - -import ( - "io" - "net/http" - - "github.com/googollee/go-engine.io/message" - "github.com/googollee/go-engine.io/parser" - "github.com/googollee/go-engine.io/transport" - "github.com/gorilla/websocket" -) - -type client struct { - conn *websocket.Conn - resp *http.Response -} - -func NewClient(r *http.Request) (transport.Client, error) { - dialer := websocket.DefaultDialer - - conn, resp, err := dialer.Dial(r.URL.String(), r.Header) - if err != nil { - return nil, err - } - - return &client{ - conn: conn, - resp: resp, - }, nil -} - -func (c *client) Response() *http.Response { - return c.resp -} - -func (c *client) NextReader() (*parser.PacketDecoder, error) { - var reader io.Reader - for { - t, r, err := c.conn.NextReader() - if err != nil { - return nil, err - } - switch t { - case websocket.TextMessage: - fallthrough - case websocket.BinaryMessage: - reader = r - return parser.NewDecoder(reader) - } - } -} - -func (c *client) NextWriter(msgType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error) { - wsType, newEncoder := websocket.TextMessage, parser.NewStringEncoder - if msgType == message.MessageBinary { - wsType, newEncoder = websocket.BinaryMessage, parser.NewBinaryEncoder - } - - w, err := c.conn.NextWriter(wsType) - if err != nil { - return nil, err - } - ret, err := newEncoder(w, packetType) - if err != nil { - return nil, err - } - return ret, nil -} - -func (c *client) Close() error { - return c.conn.Close() -} diff --git a/vendor/github.com/googollee/go-engine.io/websocket/server.go b/vendor/github.com/googollee/go-engine.io/websocket/server.go deleted file mode 100644 index d72103ff912..00000000000 --- a/vendor/github.com/googollee/go-engine.io/websocket/server.go +++ /dev/null @@ -1,81 +0,0 @@ -package websocket - -import ( - "io" - "net/http" - - "github.com/googollee/go-engine.io/message" - "github.com/googollee/go-engine.io/parser" - "github.com/googollee/go-engine.io/transport" - "github.com/gorilla/websocket" -) - -type Server struct { - callback transport.Callback - conn *websocket.Conn -} - -func NewServer(w http.ResponseWriter, r *http.Request, callback transport.Callback) (transport.Server, error) { - conn, err := websocket.Upgrade(w, r, nil, 10240, 10240) - if err != nil { - return nil, err - } - - ret := &Server{ - callback: callback, - conn: conn, - } - - go ret.serveHTTP(w, r) - - return ret, nil -} - -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) -} - -func (s *Server) NextWriter(msgType message.MessageType, packetType parser.PacketType) (io.WriteCloser, error) { - wsType, newEncoder := websocket.TextMessage, parser.NewStringEncoder - if msgType == message.MessageBinary { - wsType, newEncoder = websocket.BinaryMessage, parser.NewBinaryEncoder - } - - w, err := s.conn.NextWriter(wsType) - if err != nil { - return nil, err - } - ret, err := newEncoder(w, packetType) - if err != nil { - return nil, err - } - return ret, nil -} - -func (s *Server) Close() error { - return s.conn.Close() -} - -func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) { - defer s.callback.OnClose(s) - - for { - t, r, err := s.conn.NextReader() - if err != nil { - s.conn.Close() - return - } - - switch t { - case websocket.TextMessage: - fallthrough - case websocket.BinaryMessage: - decoder, err := parser.NewDecoder(r) - if err != nil { - return - } - s.callback.OnPacket(decoder) - decoder.Close() - } - } -} diff --git a/vendor/github.com/googollee/go-engine.io/websocket/websocket.go b/vendor/github.com/googollee/go-engine.io/websocket/websocket.go deleted file mode 100644 index a573f4907f3..00000000000 --- a/vendor/github.com/googollee/go-engine.io/websocket/websocket.go +++ /dev/null @@ -1,12 +0,0 @@ -package websocket - -import ( - "github.com/googollee/go-engine.io/transport" -) - -var Creater = transport.Creater{ - Name: "websocket", - Upgrading: true, - Server: NewServer, - Client: NewClient, -} diff --git a/vendor/github.com/googollee/go-socket.io/.deepsource.toml b/vendor/github.com/googollee/go-socket.io/.deepsource.toml new file mode 100644 index 00000000000..870dcd80d5d --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/.deepsource.toml @@ -0,0 +1,12 @@ +version = 1 + +test_patterns = ["**/*_test.go"] + +exclude_patterns = ["/_example/**"] + +[[analyzers]] +name = "go" +enabled = true + + [analyzers.meta] + import_paths = ["github.com/googollee/go-socket.io"] diff --git a/vendor/github.com/googollee/go-socket.io/.gitignore b/vendor/github.com/googollee/go-socket.io/.gitignore index 615566c1e58..8cdc6a14236 100644 --- a/vendor/github.com/googollee/go-socket.io/.gitignore +++ b/vendor/github.com/googollee/go-socket.io/.gitignore @@ -1,4 +1,12 @@ - -.DS_Store -.idea/ -go.sum \ No newline at end of file +/bin +/vendor +/vendor.pb +.DS_STORE +/.idea +/.vscode +*.o +*.out +profile.cov +*.prof +*.svg +*/node_modules \ No newline at end of file diff --git a/vendor/github.com/googollee/go-socket.io/.golangci.yml b/vendor/github.com/googollee/go-socket.io/.golangci.yml new file mode 100644 index 00000000000..5b9930d3664 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/.golangci.yml @@ -0,0 +1,8 @@ +run: + timeout: 3m + deadline: 5m + +output: + format: colored-line-number + print-issued-lines: true + print-linter-name: true diff --git a/vendor/github.com/googollee/go-socket.io/.travis.yml b/vendor/github.com/googollee/go-socket.io/.travis.yml deleted file mode 100644 index ca7753f3dd3..00000000000 --- a/vendor/github.com/googollee/go-socket.io/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: go -go: -- 1.7.x -- 1.11.x -install: - - go get "github.com/smartystreets/goconvey/convey" - - go get -v . diff --git a/vendor/github.com/googollee/go-socket.io/CONTRIBUTING.md b/vendor/github.com/googollee/go-socket.io/CONTRIBUTING.md new file mode 100644 index 00000000000..fcd9255df53 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Pull Request Process + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +Please make an issue first if the change is likely to increase. \ No newline at end of file diff --git a/vendor/github.com/googollee/go-socket.io/Makefile b/vendor/github.com/googollee/go-socket.io/Makefile new file mode 100644 index 00000000000..4f0b1ff5da8 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/Makefile @@ -0,0 +1,23 @@ +.PHONY: all +all: + go install ./... + +.PHONY: get_dev +get_dev: + go get -t ./... + +.PHONY: test +test: + go clean -testcache && go test -race -cover -covermode=atomic ./... + +.PHONY: bench +bench: + go clean -testcache && go test -bench . -benchmem ./... + +.PHONY: lint +lint: + golangci-lint run + +.PHONY: cover +cover: + go clean -testcache && go test ./... -cover -coverprofile=c.out && go tool cover -html=c.out diff --git a/vendor/github.com/googollee/go-socket.io/README.md b/vendor/github.com/googollee/go-socket.io/README.md index c662d760281..3a00dfc4c1e 100644 --- a/vendor/github.com/googollee/go-socket.io/README.md +++ b/vendor/github.com/googollee/go-socket.io/README.md @@ -1,14 +1,23 @@ -# socket.io +# go-socket.io -[![GoDoc](http://godoc.org/github.com/googollee/go-socket.io?status.svg)](http://godoc.org/github.com/googollee/go-socket.io) [![Build Status](https://travis-ci.org/googollee/go-socket.io.svg)](https://travis-ci.org/googollee/go-socket.io) +[![GoDoc](http://godoc.org/github.com/googollee/go-socket.io?status.svg)](http://godoc.org/github.com/googollee/go-socket.io) +[![Build Status](https://github.com/googollee/go-socket.io/workflows/Unit%20tests/badge.svg)](https://github.com/googollee/go-socket.io/actions/workflows/unittest.yaml) +[![Go Report Card](https://goreportcard.com/badge/github.com/googollee/go-socket.io)](https://goreportcard.com/report/github.com/googollee/go-socket.io) -**Please use v1.4 branch, or import "gopkg.in/googollee/go-socket.io.v1". I have no time to maintain master branch now** +go-socket.io is library an implementation of [Socket.IO](http://socket.io) in Golang, which is a realtime application framework. -go-socket.io is an implementation of [socket.io](http://socket.io) in golang, which is a realtime application framework. +Current this library supports 1.4 version of the Socket.IO client. It supports room, namespaces and broadcast at now. -It is compatible with latest implementation of socket.io in node.js, and supports room and namespace. +**Help wanted** This project is looking for contributors to help fix bugs and implement new features. Please check [Issue 192](https://github.com/googollee/go-socket.io/issues/192). All help is much appreciated. -* for compatability with socket.io 0.9.x, please use branch 0.9.x * +## Contents + +- [Install](#install) +- [Example](#example) +- [FAQ](#faq) +- [Engine.io](#engineio) +- [Community](#community) +- [License](#license) ## Install @@ -28,100 +37,31 @@ and use `socketio` as the package name inside the code. ## Example -Please check the example folder for details. - -```go -package main - -import ( - "log" - "net/http" - - "github.com/googollee/go-socket.io" -) - -func main() { - server, err := socketio.NewServer(nil) - if err != nil { - log.Fatal(err) - } - server.On("connection", func(so socketio.Socket) { - log.Println("on connection") - so.Join("chat") - so.On("chat message", func(msg string) { - log.Println("emit:", so.Emit("chat message", msg)) - server.BroadcastTo("chat", "chat message", msg) - }) - so.On("disconnection", func() { - log.Println("on disconnect") - }) - }) - server.On("error", func(so socketio.Socket, err error) { - log.Println("error:", err) - }) - - http.Handle("/socket.io/", server) - http.Handle("/", http.FileServer(http.Dir("./asset"))) - log.Println("Serving at localhost:5000...") - log.Fatal(http.ListenAndServe(":5000", nil)) -} -``` - -## Acknowledgements in go-socket.io 1.X.X - -[See documentation about acknowledgements](http://socket.io/docs/#sending-and-getting-data-(acknowledgements)) - -##### Sending ACK with data from SERVER to CLIENT - -* Client-side +Please check more examples into folder in project for details. [Examples](https://github.com/googollee/go-socket.io/tree/master/_examples) -```javascript - //using client-side socket.io-1.X.X.js - socket.emit('some:event', JSON.stringify(someData), function(data){ - console.log('ACK from server wtih data: ', data)); - }); -``` - -* Server-side - -```go -// The return type may vary depending on whether you will return -// In golang implementation of socket.io don't used callbacks for acknowledgement, -// but used return value, which wrapped into ack package and returned to the client's callback in JavaScript -so.On("some:event", func(msg string) string { - return msg //Sending ack with data in msg back to client, using "return statement" -}) -``` +## FAQ -##### Sending ACK with data from CLIENT to SERVER +It is some popular questions about this repository: -* Client-side +- Is this library supported socket.io version 2? + - No, but if you wanna you can help to do it. Join us in community chat Telegram +- How to use go-socket.io with CORS? + - Please see examples in [directory](https://github.com/googollee/go-socket.io/tree/master/_examples) +- What is minimal version Golang support for this library? + - We required Go 1.9 or upper! +- How to user? + - Go-socket.io compatibility with Socket.IO 0.9.x, please use branch 0.9.x * or tag go-socket.io@v0.9.1 -```javascript -//using client-side socket.io-1.X.X.js -//last parameter of "on" handler is callback for sending ack to server with data or without data -socket.on('some:event', function (msg, sendAckCb) { - //Sending ACK with data to server after receiving some:event from server - sendAckCb(JSON.stringify(data)); // for example used serializing to JSON -} -``` +## Community -* Server-side +Telegram chat: [@go_socketio](https://t.me/go_socketio) -```go -//You can use Emit or BroadcastTo with last parameter as callback for handling ack from client -//Sending packet to room "room_name" and event "some:event" -so.BroadcastTo("room_name", "some:event", dataForClient, func (so socketio.Socket, data string) { - log.Println("Client ACK with data: ", data) -}) +## Engineio -// Or +This project contains a sub-package called `engineio`. This used to be a separate package under https://github.com/googollee/go-engine.io. -so.Emit("some:event", dataForClient, func (so socketio.Socket, data string) { - log.Println("Client ACK with data: ", data) -}) -``` +It contains the `engine.io` analog implementation of the original node-package. https://github.com/socketio/engine.io It can be used without the socket.io-implementation. Please check the README.md in `engineio/`. ## License -The 3-clause BSD License - see LICENSE for more details +The 3-clause BSD License - see [LICENSE](https://opensource.org/licenses/BSD-3-Clause) for more details diff --git a/vendor/github.com/googollee/go-socket.io/adapter.go b/vendor/github.com/googollee/go-socket.io/adapter.go deleted file mode 100644 index 32b1ae88916..00000000000 --- a/vendor/github.com/googollee/go-socket.io/adapter.go +++ /dev/null @@ -1,79 +0,0 @@ -package socketio - -import "sync" - -// BroadcastAdaptor is the adaptor to handle broadcasts. -type BroadcastAdaptor interface { - - // Join causes the socket to join a room. - Join(room string, socket Socket) error - - // Leave causes the socket to leave a room. - Leave(room string, socket Socket) error - - // Send will send an event with args to the room. If "ignore" is not nil, the event will be excluded from being sent to "ignore". - Send(ignore Socket, room, event string, args ...interface{}) error - - //Len socket in room - Len(room string) int -} - -var newBroadcast = newBroadcastDefault - -type broadcast struct { - m map[string]map[string]Socket - sync.RWMutex -} - -func newBroadcastDefault() BroadcastAdaptor { - return &broadcast{ - m: make(map[string]map[string]Socket), - } -} - -func (b *broadcast) Join(room string, socket Socket) error { - b.Lock() - sockets, ok := b.m[room] - if !ok { - sockets = make(map[string]Socket) - } - sockets[socket.Id()] = socket - b.m[room] = sockets - b.Unlock() - return nil -} - -func (b *broadcast) Leave(room string, socket Socket) error { - b.Lock() - defer b.Unlock() - sockets, ok := b.m[room] - if !ok { - return nil - } - delete(sockets, socket.Id()) - if len(sockets) == 0 { - delete(b.m, room) - return nil - } - b.m[room] = sockets - return nil -} - -func (b *broadcast) Send(ignore Socket, room, event string, args ...interface{}) error { - b.RLock() - sockets := b.m[room] - for id, s := range sockets { - if ignore != nil && ignore.Id() == id { - continue - } - s.Emit(event, args...) - } - b.RUnlock() - return nil -} - -func (b *broadcast) Len(room string) int { - b.RLock() - defer b.RUnlock() - return len(b.m[room]) -} diff --git a/vendor/github.com/googollee/go-socket.io/adapter_options.go b/vendor/github.com/googollee/go-socket.io/adapter_options.go new file mode 100644 index 00000000000..7858a09a02a --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/adapter_options.go @@ -0,0 +1,64 @@ +package socketio + +import "fmt" + +// RedisAdapterOptions is configuration to create new adapter +type RedisAdapterOptions struct { + // deprecated. Usage Addr options + Host string + // deprecated. Usage Addr options + Port string + Addr string + Prefix string + Network string + Password string + // DB : specifies the database to select when dialing a connection. + DB int +} + +func (ro *RedisAdapterOptions) getAddr() string { + if ro.Addr == "" { + ro.Addr = fmt.Sprintf("%s:%s", ro.Host, ro.Port) + } + return ro.Addr +} + +func defaultOptions() *RedisAdapterOptions { + return &RedisAdapterOptions{ + Addr: "127.0.0.1:6379", + Prefix: "socket.io", + Network: "tcp", + } +} + +func getOptions(opts *RedisAdapterOptions) *RedisAdapterOptions { + options := defaultOptions() + + if opts != nil { + if opts.Host != "" { + options.Host = opts.Host + } + + if opts.Port != "" { + options.Port = opts.Port + } + + if opts.Addr != "" { + options.Addr = opts.Addr + } + + if opts.Prefix != "" { + options.Prefix = opts.Prefix + } + + if opts.Network != "" { + options.Network = opts.Network + } + + if len(opts.Password) > 0 { + options.Password = opts.Password + } + } + + return options +} diff --git a/vendor/github.com/googollee/go-socket.io/attachment.go b/vendor/github.com/googollee/go-socket.io/attachment.go deleted file mode 100644 index 2f04c082339..00000000000 --- a/vendor/github.com/googollee/go-socket.io/attachment.go +++ /dev/null @@ -1,168 +0,0 @@ -package socketio - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "reflect" -) - -// Attachment is an attachment handler used in emit args. All attachments will be sent as binary data in the transport layer. When using an attachment, make sure it is a pointer. -// -// For example: -// -// type Arg struct { -// Title string `json:"title"` -// File *Attachment `json:"file"` -// } -// -// f, _ := os.Open("./some_file") -// arg := Arg{ -// Title: "some_file", -// File: &Attachment{ -// Data: f, -// } -// } -// -// socket.Emit("send file", arg) -// socket.On("get file", func(so Socket, arg Arg) { -// b, _ := ioutil.ReadAll(arg.File.Data) -// }) -type Attachment struct { - Data io.ReadWriter - num int -} - -func encodeAttachments(v interface{}) []io.Reader { - index := 0 - return encodeAttachmentValue(reflect.ValueOf(v), &index) -} - -func encodeAttachmentValue(v reflect.Value, index *int) []io.Reader { - v = reflect.Indirect(v) - ret := []io.Reader{} - if !v.IsValid() { - return ret - } - switch v.Kind() { - case reflect.Struct: - if v.Type().Name() == "Attachment" { - a, ok := v.Addr().Interface().(*Attachment) - if !ok { - panic("can't convert") - } - a.num = *index - ret = append(ret, a.Data) - (*index)++ - return ret - } - for i, n := 0, v.NumField(); i < n; i++ { - var r []io.Reader - r = encodeAttachmentValue(v.Field(i), index) - ret = append(ret, r...) - } - case reflect.Map: - if v.IsNil() { - return ret - } - for _, key := range v.MapKeys() { - var r []io.Reader - r = encodeAttachmentValue(v.MapIndex(key), index) - ret = append(ret, r...) - } - case reflect.Slice: - if v.IsNil() { - return ret - } - fallthrough - case reflect.Array: - for i, n := 0, v.Len(); i < n; i++ { - var r []io.Reader - r = encodeAttachmentValue(v.Index(i), index) - ret = append(ret, r...) - } - case reflect.Interface: - ret = encodeAttachmentValue(reflect.ValueOf(v.Interface()), index) - } - return ret -} - -func decodeAttachments(v interface{}, binary [][]byte) error { - return decodeAttachmentValue(reflect.ValueOf(v), binary) -} - -func decodeAttachmentValue(v reflect.Value, binary [][]byte) error { - v = reflect.Indirect(v) - if !v.IsValid() { - return fmt.Errorf("invalid value") - } - switch v.Kind() { - case reflect.Struct: - if v.Type().Name() == "Attachment" { - a, ok := v.Addr().Interface().(*Attachment) - if !ok { - panic("can't convert") - } - if a.num >= len(binary) || a.num < 0 { - return fmt.Errorf("out of range") - } - if a.Data == nil { - a.Data = bytes.NewBuffer(nil) - } - for b := binary[a.num]; len(b) > 0; { - n, err := a.Data.Write(b) - if err != nil { - return err - } - b = b[n:] - } - return nil - } - for i, n := 0, v.NumField(); i < n; i++ { - if err := decodeAttachmentValue(v.Field(i), binary); err != nil { - return err - } - } - case reflect.Map: - if v.IsNil() { - return nil - } - for _, key := range v.MapKeys() { - if err := decodeAttachmentValue(v.MapIndex(key), binary); err != nil { - return err - } - } - case reflect.Slice: - if v.IsNil() { - return nil - } - fallthrough - case reflect.Array: - for i, n := 0, v.Len(); i < n; i++ { - if err := decodeAttachmentValue(v.Index(i), binary); err != nil { - return err - } - } - case reflect.Interface: - if err := decodeAttachmentValue(reflect.ValueOf(v.Interface()), binary); err != nil { - return err - } - } - return nil -} - -func (a Attachment) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf("{\"_placeholder\":true,\"num\":%d}", a.num)), nil -} - -func (a *Attachment) UnmarshalJSON(b []byte) error { - var v struct { - Num int `json:"num"` - } - if err := json.Unmarshal(b, &v); err != nil { - return err - } - a.num = v.Num - return nil -} diff --git a/vendor/github.com/googollee/go-socket.io/broadcast.go b/vendor/github.com/googollee/go-socket.io/broadcast.go new file mode 100644 index 00000000000..8d4b71c60fe --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/broadcast.go @@ -0,0 +1,167 @@ +package socketio + +import "sync" + +// EachFunc typed for each callback function +type EachFunc func(Conn) + +// Broadcast is the adaptor to handle broadcasts & rooms for socket.io server API +type Broadcast interface { + Join(room string, connection Conn) // Join causes the connection to join a room + Leave(room string, connection Conn) // Leave causes the connection to leave a room + LeaveAll(connection Conn) // LeaveAll causes given connection to leave all rooms + Clear(room string) // Clear causes removal of all connections from the room + Send(room, event string, args ...interface{}) // Send will send an event with args to the room + SendAll(event string, args ...interface{}) // SendAll will send an event with args to all the rooms + ForEach(room string, f EachFunc) // ForEach sends data by DataFunc, if room does not exits sends nothing + Len(room string) int // Len gives number of connections in the room + Rooms(connection Conn) []string // Gives list of all the rooms if no connection given, else list of all the rooms the connection joined + AllRooms() []string // Gives list of all the rooms the connection joined +} + +// broadcast gives Join, Leave & BroadcastTO server API support to socket.io along with room management +// map of rooms where each room contains a map of connection id to connections in that room +type broadcast struct { + rooms map[string]map[string]Conn + + lock sync.RWMutex +} + +// newBroadcast creates a new broadcast adapter +func newBroadcast() *broadcast { + return &broadcast{ + rooms: make(map[string]map[string]Conn), + } +} + +// Join joins the given connection to the broadcast room +func (bc *broadcast) Join(room string, connection Conn) { + bc.lock.Lock() + defer bc.lock.Unlock() + + if _, ok := bc.rooms[room]; !ok { + bc.rooms[room] = make(map[string]Conn) + } + + bc.rooms[room][connection.ID()] = connection +} + +// Leave leaves the given connection from given room (if exist) +func (bc *broadcast) Leave(room string, connection Conn) { + bc.lock.Lock() + defer bc.lock.Unlock() + + if connections, ok := bc.rooms[room]; ok { + delete(connections, connection.ID()) + + if len(connections) == 0 { + delete(bc.rooms, room) + } + } +} + +// LeaveAll leaves the given connection from all rooms +func (bc *broadcast) LeaveAll(connection Conn) { + bc.lock.Lock() + defer bc.lock.Unlock() + + for room, connections := range bc.rooms { + delete(connections, connection.ID()) + + if len(connections) == 0 { + delete(bc.rooms, room) + } + } +} + +// Clear clears the room +func (bc *broadcast) Clear(room string) { + bc.lock.Lock() + defer bc.lock.Unlock() + + delete(bc.rooms, room) +} + +// Send sends given event & args to all the connections in the specified room +func (bc *broadcast) Send(room, event string, args ...interface{}) { + bc.lock.RLock() + defer bc.lock.RUnlock() + + for _, connection := range bc.rooms[room] { + connection.Emit(event, args...) + } +} + +// SendAll sends given event & args to all the connections to all the rooms +func (bc *broadcast) SendAll(event string, args ...interface{}) { + bc.lock.RLock() + defer bc.lock.RUnlock() + + for _, connections := range bc.rooms { + for _, connection := range connections { + connection.Emit(event, args...) + } + } +} + +// ForEach sends data returned by DataFunc, if room does not exits sends nothing +func (bc *broadcast) ForEach(room string, f EachFunc) { + bc.lock.RLock() + defer bc.lock.RUnlock() + + occupants, ok := bc.rooms[room] + if !ok { + return + } + + for _, connection := range occupants { + f(connection) + } +} + +// Len gives number of connections in the room +func (bc *broadcast) Len(room string) int { + bc.lock.RLock() + defer bc.lock.RUnlock() + + return len(bc.rooms[room]) +} + +// Rooms gives the list of all the rooms available for broadcast in case of +// no connection is given, in case of a connection is given, it gives +// list of all the rooms the connection is joined to +func (bc *broadcast) Rooms(connection Conn) []string { + if connection == nil { + return bc.AllRooms() + } + + bc.lock.RLock() + defer bc.lock.RUnlock() + + return bc.getRoomsByConn(connection) +} + +// AllRooms gives list of all rooms available for broadcast +func (bc *broadcast) AllRooms() []string { + bc.lock.RLock() + defer bc.lock.RUnlock() + + rooms := make([]string, 0, len(bc.rooms)) + for room := range bc.rooms { + rooms = append(rooms, room) + } + + return rooms +} + +func (bc *broadcast) getRoomsByConn(connection Conn) []string { + var rooms []string + + for room, connections := range bc.rooms { + if _, ok := connections[connection.ID()]; ok { + rooms = append(rooms, room) + } + } + + return rooms +} diff --git a/vendor/github.com/googollee/go-socket.io/caller.go b/vendor/github.com/googollee/go-socket.io/caller.go deleted file mode 100644 index 15666b440da..00000000000 --- a/vendor/github.com/googollee/go-socket.io/caller.go +++ /dev/null @@ -1,82 +0,0 @@ -package socketio - -import ( - "errors" - "fmt" - "reflect" -) - -type caller struct { - Func reflect.Value - Args []reflect.Type - NeedSocket bool -} - -func newCaller(f interface{}) (*caller, error) { - fv := reflect.ValueOf(f) - if fv.Kind() != reflect.Func { - return nil, fmt.Errorf("f is not func") - } - ft := fv.Type() - if ft.NumIn() == 0 { - return &caller{ - Func: fv, - }, nil - } - args := make([]reflect.Type, ft.NumIn()) - for i, n := 0, ft.NumIn(); i < n; i++ { - args[i] = ft.In(i) - } - needSocket := false - if args[0].Name() == "Socket" { - args = args[1:] - needSocket = true - } - return &caller{ - Func: fv, - Args: args, - NeedSocket: needSocket, - }, nil -} - -func (c *caller) GetArgs() []interface{} { - ret := make([]interface{}, len(c.Args)) - for i, argT := range c.Args { - if argT.Kind() == reflect.Ptr { - argT = argT.Elem() - } - v := reflect.New(argT) - ret[i] = v.Interface() - } - return ret -} - -func (c *caller) Call(so Socket, args []interface{}) []reflect.Value { - var a []reflect.Value - diff := 0 - if c.NeedSocket { - diff = 1 - a = make([]reflect.Value, len(args)+1) - a[0] = reflect.ValueOf(so) - } else { - a = make([]reflect.Value, len(args)) - } - - if len(args) != len(c.Args) { - return []reflect.Value{reflect.ValueOf([]interface{}{}), reflect.ValueOf(errors.New("Arguments do not match"))} - } - - for i, arg := range args { - v := reflect.ValueOf(arg) - if c.Args[i].Kind() != reflect.Ptr { - if v.IsValid() { - v = v.Elem() - } else { - v = reflect.Zero(c.Args[i]) - } - } - a[i+diff] = v - } - - return c.Func.Call(a) -} diff --git a/vendor/github.com/googollee/go-socket.io/connection.go b/vendor/github.com/googollee/go-socket.io/connection.go new file mode 100644 index 00000000000..85391dcfb4d --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/connection.go @@ -0,0 +1,150 @@ +package socketio + +import ( + "io" + "net" + "net/http" + "net/url" + "reflect" + "sync" + + "github.com/googollee/go-socket.io/engineio" + "github.com/googollee/go-socket.io/parser" +) + +// Conn is a connection in go-socket.io +type Conn interface { + io.Closer + Namespace + + // ID returns session id + ID() string + URL() url.URL + LocalAddr() net.Addr + RemoteAddr() net.Addr + RemoteHeader() http.Header +} + +type conn struct { + engineio.Conn + + id uint64 + handlers *namespaceHandlers + namespaces *namespaces + + encoder *parser.Encoder + decoder *parser.Decoder + + writeChan chan parser.Payload + errorChan chan error + quitChan chan struct{} + + closeOnce sync.Once +} + +func newConn(engineConn engineio.Conn, handlers *namespaceHandlers) *conn { + return &conn{ + Conn: engineConn, + encoder: parser.NewEncoder(engineConn), + decoder: parser.NewDecoder(engineConn), + errorChan: make(chan error), + writeChan: make(chan parser.Payload), + quitChan: make(chan struct{}), + handlers: handlers, + namespaces: newNamespaces(), + } +} + +func (c *conn) Close() error { + var err error + + c.closeOnce.Do(func() { + // for each namespace, leave all rooms, and call the disconnect handler. + c.namespaces.Range(func(ns string, nc *namespaceConn) { + nc.LeaveAll() + + if nh, _ := c.handlers.Get(ns); nh != nil && nh.onDisconnect != nil { + nh.onDisconnect(nc, clientDisconnectMsg) + } + }) + err = c.Conn.Close() + + close(c.quitChan) + }) + + return err +} + +func (c *conn) connect() error { + rootHandler, ok := c.handlers.Get(rootNamespace) + if !ok { + return errUnavailableRootHandler + } + + root := newNamespaceConn(c, aliasRootNamespace, rootHandler.broadcast) + c.namespaces.Set(rootNamespace, root) + + root.Join(root.ID()) + + c.namespaces.Range(func(ns string, nc *namespaceConn) { + nc.SetContext(c.Conn.Context()) + }) + + header := parser.Header{ + Type: parser.Connect, + } + + if err := c.encoder.Encode(header); err != nil { + return err + } + + handler, ok := c.handlers.Get(header.Namespace) + if ok { + _, err := handler.dispatch(root, header) + return err + } + + return nil +} + +func (c *conn) nextID() uint64 { + c.id++ + + return c.id +} + +func (c *conn) write(header parser.Header, args ...reflect.Value) { + data := make([]interface{}, len(args)) + + for i := range data { + data[i] = args[i].Interface() + } + + pkg := parser.Payload{ + Header: header, + Data: data, + } + + select { + case c.writeChan <- pkg: + case <-c.quitChan: + return + } +} + +func (c *conn) onError(namespace string, err error) { + select { + case c.errorChan <- newErrorMessage(namespace, err): + case <-c.quitChan: + return + } +} + +func (c *conn) namespace(nsp string) *namespaceHandler { + handler, _ := c.handlers.Get(nsp) + return handler +} + +func (c *conn) parseArgs(types []reflect.Type) ([]reflect.Value, error) { + return c.decoder.DecodeArgs(types) +} diff --git a/vendor/github.com/googollee/go-socket.io/connection_handlers.go b/vendor/github.com/googollee/go-socket.io/connection_handlers.go new file mode 100644 index 00000000000..48ac942067d --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/connection_handlers.go @@ -0,0 +1,121 @@ +package socketio + +import ( + "log" + + "github.com/googollee/go-socket.io/parser" +) + +var readHandlerMapping = map[parser.Type]readHandler{ + parser.Ack: ackPacketHandler, + parser.Connect: connectPacketHandler, + parser.Disconnect: disconnectPacketHandler, +} + +func ackPacketHandler(c *conn, header parser.Header) error { + conn, ok := c.namespaces.Get(header.Namespace) + if !ok { + _ = c.decoder.DiscardLast() + return nil + } + + conn.dispatch(header) + + return nil +} + +func eventPacketHandler(c *conn, event string, header parser.Header) error { + conn, ok := c.namespaces.Get(header.Namespace) + if !ok { + _ = c.decoder.DiscardLast() + return nil + } + + handler, ok := c.handlers.Get(header.Namespace) + if !ok { + _ = c.decoder.DiscardLast() + return nil + } + + args, err := c.decoder.DecodeArgs(handler.getEventTypes(event)) + if err != nil { + c.onError(header.Namespace, err) + return errDecodeArgs + } + + ret, err := handler.dispatchEvent(conn, event, args...) + if err != nil { + c.onError(header.Namespace, err) + return errHandleDispatch + } + + if len(ret) > 0 { + header.Type = parser.Ack + c.write(header, ret...) + } + + return nil +} + +func connectPacketHandler(c *conn, header parser.Header) error { + if err := c.decoder.DiscardLast(); err != nil { + c.onError(header.Namespace, err) + return nil + } + + handler, ok := c.handlers.Get(header.Namespace) + if !ok { + c.onError(header.Namespace, errFailedConnectNamespace) + return errFailedConnectNamespace + } + + conn, ok := c.namespaces.Get(header.Namespace) + if !ok { + conn = newNamespaceConn(c, header.Namespace, handler.broadcast) + c.namespaces.Set(header.Namespace, conn) + conn.Join(c.ID()) + } + + _, err := handler.dispatch(conn, header) + if err != nil { + log.Println("dispatch connect packet", err) + c.onError(header.Namespace, err) + return errHandleDispatch + } + + c.write(header) + + return nil +} + +func disconnectPacketHandler(c *conn, header parser.Header) error { + args, err := c.decoder.DecodeArgs(defaultHeaderType) + if err != nil { + c.onError(header.Namespace, err) + return errDecodeArgs + } + + conn, ok := c.namespaces.Get(header.Namespace) + if !ok { + _ = c.decoder.DiscardLast() + return nil + } + + conn.LeaveAll() + + c.namespaces.Delete(header.Namespace) + + handler, ok := c.handlers.Get(header.Namespace) + if !ok { + return nil + } + + _, err = handler.dispatch(conn, header, args...) + if err != nil { + log.Println("dispatch disconnect packet", err) + c.onError(header.Namespace, err) + return errHandleDispatch + } + + return nil +} diff --git a/vendor/github.com/googollee/go-engine.io/README.md b/vendor/github.com/googollee/go-socket.io/engineio/README.md similarity index 54% rename from vendor/github.com/googollee/go-engine.io/README.md rename to vendor/github.com/googollee/go-socket.io/engineio/README.md index c5d603f5720..05c5bb237e1 100644 --- a/vendor/github.com/googollee/go-engine.io/README.md +++ b/vendor/github.com/googollee/go-socket.io/engineio/README.md @@ -1,6 +1,6 @@ # go-engine.io -[![GoDoc](http://godoc.org/github.com/googollee/go-engine.io?status.svg)](http://godoc.org/github.com/googollee/go-engine.io) [![Build Status](https://travis-ci.org/googollee/go-engine.io.svg)](https://travis-ci.org/googollee/go-engine.io) +[![GoDoc](http://godoc.org/github.com/googollee/go-socket.io/engineio?status.svg)](http://godoc.org/github.com/googollee/go-socket.io/engineio) go-engine.io is the implement of engine.io in golang, which is transport-based cross-browser/cross-device bi-directional communication layer for [go-socket.io](https://github.com/googollee/go-socket.io). @@ -11,13 +11,13 @@ It is compatible with node.js implement, and supported long-polling and websocke Install the package with: ```bash -go get github.com/googollee/go-engine.io +go get github.com/googollee/go-socket.io/engineio@v1 ``` Import it with: ```go -import "github.com/googollee/go-engine.io" +import "github.com/googollee/go-socket.io/engineio" ``` and use `engineio` as the package name inside the code. @@ -30,36 +30,33 @@ Please check example folder for details. package main import ( - "encoding/hex" "io/ioutil" "log" "net/http" - "github.com/googollee/go-engine.io" + "github.com/googollee/go-socket.io/engineio" ) func main() { - server, err := engineio.NewServer(nil) - if err != nil { - log.Fatal(err) - } + server := engineio.NewServer(nil) go func() { for { - conn, _ := server.Accept() + conn, err := server.Accept() + if err != nil { + log.Fatalln("accept error:", err) + } + go func() { defer conn.Close() - for i := 0; i < 10; i++ { + + for { t, r, _ := conn.NextReader() b, _ := ioutil.ReadAll(r) r.Close() - if t == engineio.MessageText { - log.Println(t, string(b)) - } else { - log.Println(t, hex.EncodeToString(b)) - } + w, _ := conn.NextWriter(t) - w.Write([]byte("pong")) + w.Write(b) w.Close() } }() @@ -67,12 +64,12 @@ func main() { }() http.Handle("/engine.io/", server) - http.Handle("/", http.FileServer(http.Dir("./asset"))) log.Println("Serving at localhost:5000...") + log.Fatal(http.ListenAndServe(":5000", nil)) } ``` ## License -The 3-clause BSD License - see LICENSE for more details \ No newline at end of file +The 3-clause BSD License - see [LICENSE](https://opensource.org/licenses/BSD-3-Clause) for more details diff --git a/vendor/github.com/googollee/go-socket.io/engineio/client.go b/vendor/github.com/googollee/go-socket.io/engineio/client.go new file mode 100644 index 00000000000..5f56455c16e --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/client.go @@ -0,0 +1,129 @@ +package engineio + +import ( + "fmt" + "io" + "net" + "net/http" + "net/url" + "sync" + "time" + + "github.com/googollee/go-socket.io/engineio/frame" + "github.com/googollee/go-socket.io/engineio/packet" + "github.com/googollee/go-socket.io/engineio/session" + "github.com/googollee/go-socket.io/engineio/transport" +) + +// Pauser is connection which can be paused and resumes. +type Pauser interface { + Pause() + Resume() +} + +// Opener is client connection which need receive open message first. +type Opener interface { + Open() (transport.ConnParameters, error) +} + +type client struct { + conn transport.Conn + params transport.ConnParameters + transport string + context interface{} + close chan struct{} + closeOnce sync.Once +} + +func (c *client) SetContext(v interface{}) { + c.context = v +} + +func (c *client) Context() interface{} { + return c.context +} + +func (c *client) ID() string { + return c.params.SID +} + +func (c *client) Transport() string { + return c.transport +} + +func (c *client) Close() error { + c.closeOnce.Do(func() { + close(c.close) + }) + return c.conn.Close() +} + +func (c *client) NextReader() (session.FrameType, io.ReadCloser, error) { + for { + ft, pt, r, err := c.conn.NextReader() + if err != nil { + return 0, nil, err + } + + switch pt { + case packet.PONG: + if err = c.conn.SetReadDeadline(time.Now().Add(c.params.PingInterval + c.params.PingTimeout)); err != nil { + return 0, nil, err + } + + case packet.CLOSE: + c.Close() + return 0, nil, io.EOF + + case packet.MESSAGE: + return session.FrameType(ft), r, nil + } + + r.Close() + } +} + +func (c *client) NextWriter(typ session.FrameType) (io.WriteCloser, error) { + return c.conn.NextWriter(frame.Type(typ), packet.MESSAGE) +} + +func (c *client) URL() url.URL { + return c.conn.URL() +} + +func (c *client) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *client) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +func (c *client) RemoteHeader() http.Header { + return c.conn.RemoteHeader() +} + +func (c *client) serve() { + defer c.conn.Close() + + for { + select { + case <-c.close: + return + case <-time.After(c.params.PingInterval): + } + + w, err := c.conn.NextWriter(frame.String, packet.PING) + if err != nil { + return + } + + if err := w.Close(); err != nil { + return + } + + if err = c.conn.SetWriteDeadline(time.Now().Add(c.params.PingInterval + c.params.PingTimeout)); err != nil { + fmt.Printf("set writer's deadline error,msg:%s\n", err.Error()) + } + } +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/connect.go b/vendor/github.com/googollee/go-socket.io/engineio/connect.go new file mode 100644 index 00000000000..47444b2a0a1 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/connect.go @@ -0,0 +1,24 @@ +package engineio + +import ( + "io" + "net" + "net/http" + "net/url" + + "github.com/googollee/go-socket.io/engineio/session" +) + +// Conn is connection by client session +type Conn interface { + ID() string + NextReader() (session.FrameType, io.ReadCloser, error) + NextWriter(fType session.FrameType) (io.WriteCloser, error) + Close() error + URL() url.URL + LocalAddr() net.Addr + RemoteAddr() net.Addr + RemoteHeader() http.Header + SetContext(v interface{}) + Context() interface{} +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/dialer.go b/vendor/github.com/googollee/go-socket.io/engineio/dialer.go new file mode 100644 index 00000000000..d5351999fb4 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/dialer.go @@ -0,0 +1,84 @@ +package engineio + +import ( + "errors" + "io" + "net/http" + "net/url" + + "github.com/googollee/go-socket.io/engineio/packet" + "github.com/googollee/go-socket.io/engineio/transport" +) + +// Dialer is dialer configure. +type Dialer struct { + Transports []transport.Transport +} + +// Dial returns a connection which dials to url with requestHeader. +func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (Conn, error) { + u, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + query := u.Query() + query.Set("EIO", "3") + u.RawQuery = query.Encode() + + var conn transport.Conn + + for i := len(d.Transports) - 1; i >= 0; i-- { + if conn != nil { + conn.Close() + } + + t := d.Transports[i] + + conn, err = t.Dial(u, requestHeader) + if err != nil { + continue + } + + var params transport.ConnParameters + if p, ok := conn.(Opener); ok { + params, err = p.Open() + if err != nil { + continue + } + } else { + var pt packet.Type + var r io.ReadCloser + _, pt, r, err = conn.NextReader() + if err != nil { + continue + } + func() { + defer r.Close() + if pt != packet.OPEN { + err = errors.New("invalid open") + return + } + params, err = transport.ReadConnParameters(r) + if err != nil { + return + } + }() + } + if err != nil { + continue + } + ret := &client{ + conn: conn, + params: params, + transport: t.Name(), + close: make(chan struct{}), + } + + go ret.serve() + + return ret, nil + } + + return nil, err +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/frame/frame.go b/vendor/github.com/googollee/go-socket.io/engineio/frame/frame.go new file mode 100644 index 00000000000..573c7115e51 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/frame/frame.go @@ -0,0 +1,21 @@ +package frame + +// Type is the type of frames. +type Type byte + +const ( + // String identifies a string frame. + String Type = iota + // Binary identifies a binary frame. + Binary +) + +// ByteToFrameType converts a byte to FrameType. +func ByteToFrameType(b byte) Type { + return Type(b) +} + +// Byte returns type in byte. +func (t Type) Byte() byte { + return byte(t) +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/packet/decoder.go b/vendor/github.com/googollee/go-socket.io/engineio/packet/decoder.go new file mode 100644 index 00000000000..85e93f49fc5 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/packet/decoder.go @@ -0,0 +1,35 @@ +package packet + +import ( + "io" + + "github.com/googollee/go-socket.io/engineio/frame" +) + +// FrameReader is the reader which supports framing. +type FrameReader interface { + NextReader() (frame.Type, io.ReadCloser, error) +} + +type Decoder struct { + r FrameReader +} + +func NewDecoder(r FrameReader) *Decoder { + return &Decoder{ + r: r, + } +} + +func (e *Decoder) NextReader() (frame.Type, Type, io.ReadCloser, error) { + ft, r, err := e.r.NextReader() + if err != nil { + return 0, 0, nil, err + } + var b [1]byte + if _, err := io.ReadFull(r, b[:]); err != nil { + _ = r.Close() + return 0, 0, nil, err + } + return ft, ByteToPacketType(b[0], ft), r, nil +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/packet/encoder.go b/vendor/github.com/googollee/go-socket.io/engineio/packet/encoder.go new file mode 100644 index 00000000000..51c93aab5cb --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/packet/encoder.go @@ -0,0 +1,43 @@ +package packet + +import ( + "io" + + "github.com/googollee/go-socket.io/engineio/frame" +) + +// FrameWriter is the writer which supports framing. +type FrameWriter interface { + NextWriter(typ frame.Type) (io.WriteCloser, error) +} + +type Encoder struct { + w FrameWriter +} + +func NewEncoder(w FrameWriter) *Encoder { + return &Encoder{ + w: w, + } +} + +func (e *Encoder) NextWriter(ft frame.Type, pt Type) (io.WriteCloser, error) { + w, err := e.w.NextWriter(ft) + if err != nil { + return nil, err + } + + var b [1]byte + if ft == frame.String { + b[0] = pt.StringByte() + } else { + b[0] = pt.BinaryByte() + } + + if _, err := w.Write(b[:]); err != nil { + w.Close() + return nil, err + } + + return w, nil +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_discarder.go b/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_discarder.go new file mode 100644 index 00000000000..ccb404cfccc --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_discarder.go @@ -0,0 +1,23 @@ +package packet + +import ( + "io" + + "github.com/googollee/go-socket.io/engineio/frame" +) + +type fakeOneFrameDiscarder struct{} + +func (d fakeOneFrameDiscarder) Write(p []byte) (int, error) { + return len(p), nil +} + +func (d fakeOneFrameDiscarder) Close() error { + return nil +} + +type FakeDiscardWriter struct{} + +func (w *FakeDiscardWriter) NextWriter(fType frame.Type) (io.WriteCloser, error) { + return fakeOneFrameDiscarder{}, nil +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_frame.go b/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_frame.go new file mode 100644 index 00000000000..3bb879e4c79 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_frame.go @@ -0,0 +1,40 @@ +package packet + +import ( + "bytes" + + "github.com/googollee/go-socket.io/engineio/frame" +) + +type fakeFrame struct { + w *fakeConnWriter + typ frame.Type + data *bytes.Buffer +} + +func newFakeFrame(w *fakeConnWriter, fType frame.Type) *fakeFrame { + return &fakeFrame{ + w: w, + typ: fType, + data: bytes.NewBuffer(nil), + } +} + +func (w *fakeFrame) Write(p []byte) (int, error) { + return w.data.Write(p) +} + +func (w *fakeFrame) Read(p []byte) (int, error) { + return w.data.Read(p) +} + +func (w *fakeFrame) Close() error { + if w.w == nil { + return nil + } + w.w.Frames = append(w.w.Frames, Frame{ + FType: w.typ, + Data: w.data.Bytes(), + }) + return nil +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_reader.go b/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_reader.go new file mode 100644 index 00000000000..8963fa667d5 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_reader.go @@ -0,0 +1,64 @@ +package packet + +import ( + "bytes" + "io" + "io/ioutil" + + "github.com/googollee/go-socket.io/engineio/frame" +) + +type fakeConnReader struct { + frames []Frame +} + +func NewFakeConnReader(frames []Frame) *fakeConnReader { + return &fakeConnReader{ + frames: frames, + } +} + +func (r *fakeConnReader) NextReader() (frame.Type, io.ReadCloser, error) { + if len(r.frames) == 0 { + return frame.String, nil, io.EOF + } + f := r.frames[0] + r.frames = r.frames[1:] + return f.FType, ioutil.NopCloser(bytes.NewReader(f.Data)), nil +} + +type fakeOneFrameConst struct { + b byte +} + +func (c *fakeOneFrameConst) Read(p []byte) (int, error) { + p[0] = c.b + return 1, nil +} + +type fakeConstReader struct { + ft frame.Type + r *fakeOneFrameConst +} + +func NewFakeConstReader() *fakeConstReader { + return &fakeConstReader{ + ft: frame.String, + r: &fakeOneFrameConst{ + b: MESSAGE.StringByte(), + }, + } +} + +func (r *fakeConstReader) NextReader() (frame.Type, io.ReadCloser, error) { + ft := r.ft + switch ft { + case frame.Binary: + r.ft = frame.String + r.r.b = MESSAGE.StringByte() + case frame.String: + r.ft = frame.Binary + r.r.b = MESSAGE.BinaryByte() + } + return ft, ioutil.NopCloser(r.r), nil +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_writer.go b/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_writer.go new file mode 100644 index 00000000000..c896980d84d --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/packet/fake_writer.go @@ -0,0 +1,19 @@ +package packet + +import ( + "io" + + "github.com/googollee/go-socket.io/engineio/frame" +) + +type fakeConnWriter struct { + Frames []Frame +} + +func NewFakeConnWriter() *fakeConnWriter { + return &fakeConnWriter{} +} + +func (w *fakeConnWriter) NextWriter(fType frame.Type) (io.WriteCloser, error) { + return newFakeFrame(w, fType), nil +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/packet/packet.go b/vendor/github.com/googollee/go-socket.io/engineio/packet/packet.go new file mode 100644 index 00000000000..a14401b313c --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/packet/packet.go @@ -0,0 +1,68 @@ +package packet + +import "github.com/googollee/go-socket.io/engineio/frame" + +// Type is the type of packet +type Type int + +const ( + // OPEN is sent from the server when a new transport is opened (recheck). + OPEN Type = iota + // CLOSE is request the close of this transport but does not shutdown the + // connection itself. + CLOSE + // PING is sent by the client. Server should answer with a pong packet + // containing the same data. + PING + // PONG is sent by the server to respond to ping packets. + PONG + // MESSAGE is actual message, client and server should call their callbacks + // with the data. + MESSAGE + // UPGRADE is sent before engine.io switches a transport to test if server + // and client can communicate over this transport. If this test succeed, + // the client sends an upgrade packets which requests the server to flush + // its cache on the old transport and switch to the new transport. + UPGRADE + // NOOP is a noop packet. Used primarily to force a poll cycle when an + // incoming websocket connection is received. + NOOP +) + +func (id Type) String() string { + switch id { + case OPEN: + return "open" + case CLOSE: + return "close" + case PING: + return "ping" + case PONG: + return "pong" + case MESSAGE: + return "message" + case UPGRADE: + return "upgrade" + case NOOP: + return "noop" + } + return "unknown" +} + +// StringByte converts a PacketType to byte in string. +func (id Type) StringByte() byte { + return byte(id) + '0' +} + +// BinaryByte converts a PacketType to byte in binary. +func (id Type) BinaryByte() byte { + return byte(id) +} + +// ByteToPacketType converts a byte to PacketType. +func ByteToPacketType(b byte, typ frame.Type) Type { + if typ == frame.String { + b -= '0' + } + return Type(b) +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/packet/types.go b/vendor/github.com/googollee/go-socket.io/engineio/packet/types.go new file mode 100644 index 00000000000..8bd9e51cf53 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/packet/types.go @@ -0,0 +1,16 @@ +package packet + +import ( + "github.com/googollee/go-socket.io/engineio/frame" +) + +type Frame struct { + FType frame.Type + Data []byte +} + +type Packet struct { + FType frame.Type + PType Type + Data []byte +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/payload/decoder.go b/vendor/github.com/googollee/go-socket.io/engineio/payload/decoder.go new file mode 100644 index 00000000000..6fff1e6b257 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/payload/decoder.go @@ -0,0 +1,160 @@ +package payload + +import ( + "bufio" + "encoding/base64" + "io" + "io/ioutil" + + "github.com/googollee/go-socket.io/engineio/frame" + "github.com/googollee/go-socket.io/engineio/packet" +) + +type byteReader interface { + ReadByte() (byte, error) + io.Reader +} + +type readerFeeder interface { + getReader() (io.Reader, bool, error) + putReader(error) error +} + +type decoder struct { + b64Reader io.Reader + limitReader io.LimitedReader + rawReader byteReader + feeder readerFeeder + + ft frame.Type + pt packet.Type + supportBinary bool +} + +func (d *decoder) NextReader() (frame.Type, packet.Type, io.ReadCloser, error) { + if d.rawReader == nil { + r, supportBinary, err := d.feeder.getReader() + if err != nil { + return 0, 0, nil, err + } + br, ok := r.(byteReader) + if !ok { + br = bufio.NewReader(r) + } + if err := d.setNextReader(br, supportBinary); err != nil { + return 0, 0, nil, d.sendError(err) + } + } + + return d.ft, d.pt, d, nil +} + +func (d *decoder) Read(p []byte) (int, error) { + if d.b64Reader != nil { + return d.b64Reader.Read(p) + } + return d.limitReader.Read(p) +} + +func (d *decoder) Close() error { + if _, err := io.Copy(ioutil.Discard, d); err != nil { + return d.sendError(err) + } + err := d.setNextReader(d.rawReader, d.supportBinary) + if err != nil { + if err != io.EOF { + return d.sendError(err) + } + d.rawReader = nil + d.limitReader.R = nil + d.limitReader.N = 0 + d.b64Reader = nil + err = d.sendError(nil) + } + return err +} + +func (d *decoder) setNextReader(r byteReader, supportBinary bool) error { + var read func(byteReader) (frame.Type, packet.Type, int64, error) + if supportBinary { + read = d.binaryRead + } else { + read = d.textRead + } + + ft, pt, l, err := read(r) + if err != nil { + return err + } + + d.ft = ft + d.pt = pt + d.rawReader = r + d.limitReader.R = r + d.limitReader.N = l + d.supportBinary = supportBinary + if !supportBinary && ft == frame.Binary { + d.b64Reader = base64.NewDecoder(base64.StdEncoding, &d.limitReader) + } else { + d.b64Reader = nil + } + return nil +} + +func (d *decoder) sendError(err error) error { + if e := d.feeder.putReader(err); e != nil { + return e + } + return err +} + +func (d *decoder) textRead(r byteReader) (frame.Type, packet.Type, int64, error) { + l, err := readTextLen(r) + if err != nil { + return 0, 0, 0, err + } + + ft := frame.String + b, err := r.ReadByte() + if err != nil { + return 0, 0, 0, err + } + l-- + + if b == 'b' { + ft = frame.Binary + b, err = r.ReadByte() + if err != nil { + return 0, 0, 0, err + } + l-- + } + + pt := packet.ByteToPacketType(b, frame.String) + return ft, pt, l, nil +} + +func (d *decoder) binaryRead(r byteReader) (frame.Type, packet.Type, int64, error) { + b, err := r.ReadByte() + if err != nil { + return 0, 0, 0, err + } + if b > 1 { + return 0, 0, 0, errInvalidPayload + } + ft := frame.ByteToFrameType(b) + + l, err := readBinaryLen(r) + if err != nil { + return 0, 0, 0, err + } + + b, err = r.ReadByte() + if err != nil { + return 0, 0, 0, err + } + pt := packet.ByteToPacketType(b, ft) + l-- + + return ft, pt, l, nil +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/payload/encoder.go b/vendor/github.com/googollee/go-socket.io/engineio/payload/encoder.go new file mode 100644 index 00000000000..2ba9657e28b --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/payload/encoder.go @@ -0,0 +1,127 @@ +package payload + +import ( + "bytes" + "encoding/base64" + "io" + + "github.com/googollee/go-socket.io/engineio/frame" + "github.com/googollee/go-socket.io/engineio/packet" +) + +type writerFeeder interface { + getWriter() (io.Writer, error) + putWriter(error) error +} + +type encoder struct { + supportBinary bool + feeder writerFeeder + + ft frame.Type + pt packet.Type + header bytes.Buffer + frameCache bytes.Buffer + b64Writer io.WriteCloser + rawWriter io.Writer +} + +func (e *encoder) NOOP() []byte { + if e.supportBinary { + return []byte{0x00, 0x01, 0xff, '6'} + } + return []byte("1:6") +} + +func (e *encoder) NextWriter(ft frame.Type, pt packet.Type) (io.WriteCloser, error) { + w, err := e.feeder.getWriter() + if err != nil { + return nil, err + } + e.rawWriter = w + + e.ft = ft + e.pt = pt + e.frameCache.Reset() + + if !e.supportBinary && ft == frame.Binary { + e.b64Writer = base64.NewEncoder(base64.StdEncoding, &e.frameCache) + } else { + e.b64Writer = nil + } + return e, nil +} + +func (e *encoder) Write(p []byte) (int, error) { + if e.b64Writer != nil { + return e.b64Writer.Write(p) + } + return e.frameCache.Write(p) +} + +func (e *encoder) Close() error { + if e.b64Writer != nil { + e.b64Writer.Close() + } + + var writeHeader func() error + if e.supportBinary { + writeHeader = e.writeBinaryHeader + } else { + if e.ft == frame.Binary { + writeHeader = e.writeB64Header + } else { + writeHeader = e.writeTextHeader + } + } + + e.header.Reset() + err := writeHeader() + if err == nil { + _, err = e.header.WriteTo(e.rawWriter) + } + if err == nil { + _, err = e.frameCache.WriteTo(e.rawWriter) + } + if werr := e.feeder.putWriter(err); werr != nil { + return werr + } + return err +} + +func (e *encoder) writeTextHeader() error { + l := int64(e.frameCache.Len() + 1) // length for packet type + err := writeTextLen(l, &e.header) + if err == nil { + err = e.header.WriteByte(e.pt.StringByte()) + } + return err +} + +func (e *encoder) writeB64Header() error { + l := int64(e.frameCache.Len() + 2) // length for 'b' and packet type + err := writeTextLen(l, &e.header) + if err == nil { + err = e.header.WriteByte('b') + } + if err == nil { + err = e.header.WriteByte(e.pt.StringByte()) + } + return err +} + +func (e *encoder) writeBinaryHeader() error { + l := int64(e.frameCache.Len() + 1) // length for packet type + b := e.pt.StringByte() + if e.ft == frame.Binary { + b = e.pt.BinaryByte() + } + err := e.header.WriteByte(e.ft.Byte()) + if err == nil { + err = writeBinaryLen(l, &e.header) + } + if err == nil { + err = e.header.WriteByte(b) + } + return err +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/payload/errors.go b/vendor/github.com/googollee/go-socket.io/engineio/payload/errors.go new file mode 100644 index 00000000000..732a0a10587 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/payload/errors.go @@ -0,0 +1,57 @@ +package payload + +import ( + "errors" + "fmt" +) + +// Error is payload error. +type Error interface { + Error() string + Temporary() bool +} + +// OpError is operation error. +type OpError struct { + Op string + Err error +} + +func newOpError(op string, err error) error { + return &OpError{ + Op: op, + Err: err, + } +} + +func (e *OpError) Error() string { + return fmt.Sprintf("%s: %s", e.Op, e.Err.Error()) +} + +// Temporary returns true if error can retry. +func (e *OpError) Temporary() bool { + if oe, ok := e.Err.(Error); ok { + return oe.Temporary() + } + return false +} + +type retryError struct { + err string +} + +func (e retryError) Error() string { + return e.err +} + +func (e retryError) Temporary() bool { + return true +} + +var errPaused = retryError{"paused"} + +var errTimeout = errors.New("timeout") + +var errInvalidPayload = errors.New("invalid payload") + +var errOverlap = errors.New("overlap") diff --git a/vendor/github.com/googollee/go-socket.io/engineio/payload/pauser.go b/vendor/github.com/googollee/go-socket.io/engineio/payload/pauser.go new file mode 100644 index 00000000000..6f135affd28 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/payload/pauser.go @@ -0,0 +1,96 @@ +package payload + +import "sync" + +type pauserStatus int + +const ( + statusNormal pauserStatus = iota + statusPausing + statusPaused +) + +type pauser struct { + l sync.Mutex + c *sync.Cond + worker int + pausing chan struct{} + paused chan struct{} + status pauserStatus +} + +func newPauser() *pauser { + ret := &pauser{ + pausing: make(chan struct{}), + paused: make(chan struct{}), + status: statusNormal, + } + ret.c = sync.NewCond(&ret.l) + return ret +} + +func (p *pauser) Pause() bool { + p.l.Lock() + defer p.l.Unlock() + + switch p.status { + case statusPaused: + return false + case statusNormal: + close(p.pausing) + p.status = statusPausing + } + + for p.worker != 0 { + p.c.Wait() + } + + if p.status == statusPaused { + return false + } + close(p.paused) + p.status = statusPaused + p.c.Broadcast() + + return true +} + +func (p *pauser) Resume() { + p.l.Lock() + defer p.l.Unlock() + p.status = statusNormal + p.paused = make(chan struct{}) + p.pausing = make(chan struct{}) +} + +func (p *pauser) Working() bool { + p.l.Lock() + defer p.l.Unlock() + if p.status == statusPaused { + return false + } + p.worker++ + return true +} + +func (p *pauser) Done() { + p.l.Lock() + defer p.l.Unlock() + if p.status == statusPaused || p.worker == 0 { + return + } + p.worker-- + p.c.Broadcast() +} + +func (p *pauser) PausingTrigger() <-chan struct{} { + p.l.Lock() + defer p.l.Unlock() + return p.pausing +} + +func (p *pauser) PausedTrigger() <-chan struct{} { + p.l.Lock() + defer p.l.Unlock() + return p.paused +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/payload/payload.go b/vendor/github.com/googollee/go-socket.io/engineio/payload/payload.go new file mode 100644 index 00000000000..5d03bc9a6d2 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/payload/payload.go @@ -0,0 +1,401 @@ +package payload + +import ( + "io" + "math" + "sync" + "sync/atomic" + "time" + + "github.com/googollee/go-socket.io/engineio/frame" + "github.com/googollee/go-socket.io/engineio/packet" +) + +type readArg struct { + r io.Reader + supportBinary bool +} + +// Payload does encode or decode to payload protocol. +type Payload struct { + close chan struct{} + closeOnce sync.Once + err atomic.Value + + pauser *pauser + + readerChan chan readArg + feeding int32 + readError chan error + readDeadline atomic.Value + decoder decoder + + writerChan chan io.Writer + flushing int32 + writeError chan error + writeDeadline atomic.Value + encoder encoder +} + +// New returns a new payload. +func New(supportBinary bool) *Payload { + ret := &Payload{ + close: make(chan struct{}), + pauser: newPauser(), + readerChan: make(chan readArg), + readError: make(chan error), + writerChan: make(chan io.Writer), + writeError: make(chan error), + } + ret.readDeadline.Store(time.Time{}) + ret.decoder.feeder = ret + ret.writeDeadline.Store(time.Time{}) + ret.encoder.supportBinary = supportBinary + ret.encoder.feeder = ret + return ret +} + +// FeedIn feeds in a new reader for NextReader. +// Multi-FeedIn needs be called sync. +// +// If Close called when FeedIn, it returns io.EOF. +// If have Pause-ed when FeedIn, it returns ErrPaused. +// If NextReader has timeout, it returns ErrTimeout. +// If read error while FeedIn, it returns read error. +func (p *Payload) FeedIn(r io.Reader, supportBinary bool) error { + select { + case <-p.close: + return p.load() + default: + } + + if !atomic.CompareAndSwapInt32(&p.feeding, 0, 1) { + return newOpError("read", errOverlap) + } + defer atomic.StoreInt32(&p.feeding, 0) + + if ok := p.pauser.Working(); !ok { + return newOpError("payload", errPaused) + } + defer p.pauser.Done() + + for { + after, ok := p.readTimeout() + if !ok { + return p.Store("read", errTimeout) + } + + select { + case <-p.close: + return p.load() + + case <-after: + // it may changed during wait, need check again + continue + + case p.readerChan <- readArg{ + r: r, + supportBinary: supportBinary, + }: + } + break + } + + for { + after, ok := p.readTimeout() + if !ok { + return p.Store("read", errTimeout) + } + + select { + case <-after: + // it may changed during wait, need check again + continue + + case err := <-p.readError: + return p.Store("read", err) + } + } +} + +// FlushOut write data from NextWriter. +// FlushOut needs be called sync. +// +// If Close called when Flushout, it return io.EOF. +// If Pause called when Flushout, it flushs out a NOOP message and return +// nil. +// If NextWriter has timeout, it returns ErrTimeout. +// If write error while FlushOut, it returns write error. +func (p *Payload) FlushOut(w io.Writer) error { + select { + case <-p.close: + return p.load() + default: + } + + if !atomic.CompareAndSwapInt32(&p.flushing, 0, 1) { + return newOpError("write", errOverlap) + } + defer atomic.StoreInt32(&p.flushing, 0) + + if ok := p.pauser.Working(); !ok { + _, err := w.Write(p.encoder.NOOP()) + return err + } + defer p.pauser.Done() + + for { + after, ok := p.writeTimeout() + if !ok { + return p.Store("write", errTimeout) + } + select { + case <-p.close: + return p.load() + + case <-after: + continue + + case <-p.pauser.PausingTrigger(): + _, err := w.Write(p.encoder.NOOP()) + return err + + case p.writerChan <- w: + } + break + } + + for { + after, ok := p.writeTimeout() + if !ok { + return p.Store("write", errTimeout) + } + select { + case <-after: + // it may changed during wait, need check again + case err := <-p.writeError: + return p.Store("write", err) + } + } +} + +// NextReader returns a reader for next frame. +// NextReader and SetReadDeadline needs be called sync. +// +// If Close called when NextReader, it return io.EOF. +// Pause doesn't effect to NextReader. NextReader should wait till resumed +// and next FeedIn. +func (p *Payload) NextReader() (frame.Type, packet.Type, io.ReadCloser, error) { + ft, pt, r, err := p.decoder.NextReader() + return ft, pt, r, err +} + +// SetReadDeadline sets next reader deadline. +// NextReader and SetReadDeadline needs be called sync. +// NextReader will wait a FeedIn call, then it returns ReadCloser which +// decodes packet from FeedIn's Reader. +// +// If Close called when SetReadDeadline, it return io.EOF. +// If beyond the time set by SetReadDeadline, it returns ErrTimeout. +// Pause doesn't effect to SetReadDeadline. +func (p *Payload) SetReadDeadline(t time.Time) error { + p.readDeadline.Store(t) + return nil +} + +// NextWriter returns a writer for next frame. +// NextWriter and SetWriterDeadline needs be called sync. +// NextWriter will wait a FlushOut call, then it returns WriteCloser which +// encode package to FlushOut's Writer. +// +// If Close called when NextWriter, it returns io.EOF. +// If beyond the time set by SetWriteDeadline, it returns ErrTimeout. +// If Pause called when NextWriter, it returns ErrPaused. +func (p *Payload) NextWriter(ft frame.Type, pt packet.Type) (io.WriteCloser, error) { + return p.encoder.NextWriter(ft, pt) +} + +// SetWriteDeadline sets next writer deadline. +// NextWriter and SetWriteDeadline needs be called sync. +// +// If Close called when SetWriteDeadline, it return io.EOF. +// Pause doesn't effect to SetWriteDeadline. +func (p *Payload) SetWriteDeadline(t time.Time) error { + p.writeDeadline.Store(t) + return nil +} + +// Pause pauses the payload. It will wait all reader and writer closed which +// created from NextReader or NextWriter. +// It can call in multi-goroutine. +func (p *Payload) Pause() { + p.pauser.Pause() +} + +// Resume resumes the payload. +// It can call in multi-goroutine. +func (p *Payload) Resume() { + p.pauser.Resume() +} + +// Close closes the payload. +// It can call in multi-goroutine. +func (p *Payload) Close() error { + p.closeOnce.Do(func() { + close(p.close) + }) + return nil +} + +// Store stores a error in payload, and block all other request. +func (p *Payload) Store(op string, err error) error { + old := p.err.Load() + if old == nil { + if err == io.EOF || err == nil { + return err + } + op := newOpError(op, err) + p.err.Store(op) + return op + } + return old.(error) +} + +func (p *Payload) readTimeout() (<-chan time.Time, bool) { + deadline := p.readDeadline.Load().(time.Time) + wait := time.Until(deadline) + if deadline.IsZero() { + // wait for every + wait = math.MaxInt64 + } + if wait <= 0 { + return nil, false + } + return time.After(wait), true +} + +func (p *Payload) writeTimeout() (<-chan time.Time, bool) { + deadline := p.writeDeadline.Load().(time.Time) + wait := time.Until(deadline) + if deadline.IsZero() { + // wait for every + wait = math.MaxInt64 + } + if wait <= 0 { + return nil, false + } + return time.After(wait), true +} + +func (p *Payload) getReader() (io.Reader, bool, error) { + select { + case <-p.close: + return nil, false, p.load() + default: + } + + if ok := p.pauser.Working(); !ok { + return nil, false, newOpError("payload", errPaused) + } + p.pauser.Done() + + for { + after, ok := p.readTimeout() + if !ok { + return nil, false, p.Store("read", errTimeout) + } + select { + case <-p.close: + return nil, false, p.load() + case <-p.pauser.PausedTrigger(): + return nil, false, newOpError("payload", errPaused) + case <-after: + continue + case arg := <-p.readerChan: + return arg.r, arg.supportBinary, nil + } + } +} + +func (p *Payload) putReader(err error) error { + select { + case <-p.close: + return p.load() + default: + } + for { + after, ok := p.readTimeout() + if !ok { + return p.Store("read", errTimeout) + } + select { + case <-p.close: + return p.load() + case <-after: + continue + case p.readError <- err: + } + return nil + } +} + +func (p *Payload) getWriter() (io.Writer, error) { + select { + case <-p.close: + return nil, p.load() + default: + } + + if ok := p.pauser.Working(); !ok { + return nil, newOpError("payload", errPaused) + } + p.pauser.Done() + + for { + after, ok := p.writeTimeout() + if !ok { + return nil, p.Store("write", errTimeout) + } + select { + case <-p.close: + return nil, p.load() + case <-p.pauser.PausedTrigger(): + return nil, newOpError("payload", errPaused) + case <-after: + continue + case w := <-p.writerChan: + return w, nil + } + } +} + +func (p *Payload) putWriter(err error) error { + select { + case <-p.close: + return p.load() + default: + } + for { + after, ok := p.writeTimeout() + if !ok { + return p.Store("write", errTimeout) + } + ret := p.Store("write", err) + select { + case <-p.close: + return p.load() + case <-after: + continue + case p.writeError <- err: + return ret + } + } +} + +func (p *Payload) load() error { + ret := p.err.Load() + if ret == nil { + return io.EOF + } + return ret.(error) +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/payload/util.go b/vendor/github.com/googollee/go-socket.io/engineio/payload/util.go new file mode 100644 index 00000000000..df096e17da5 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/payload/util.go @@ -0,0 +1,89 @@ +package payload + +import "bytes" + +func writeBinaryLen(l int64, w *bytes.Buffer) error { + if l <= 0 { + if err := w.WriteByte(0x00); err != nil { + return err + } + if err := w.WriteByte(0xff); err != nil { + return err + } + return nil + } + max := int64(1) + for n := l / 10; n > 0; n /= 10 { + max *= 10 + } + for max > 0 { + n := l / max + if err := w.WriteByte(byte(n)); err != nil { + return err + } + l -= n * max + max /= 10 + } + return w.WriteByte(0xff) +} + +func writeTextLen(l int64, w *bytes.Buffer) error { + if l <= 0 { + if err := w.WriteByte('0'); err != nil { + return err + } + if err := w.WriteByte(':'); err != nil { + return err + } + return nil + } + max := int64(1) + for n := l / 10; n > 0; n /= 10 { + max *= 10 + } + for max > 0 { + n := l / max + if err := w.WriteByte(byte(n) + '0'); err != nil { + return err + } + l -= n * max + max /= 10 + } + return w.WriteByte(':') +} + +func readBinaryLen(r byteReader) (int64, error) { + ret := int64(0) + for { + b, err := r.ReadByte() + if err != nil { + return 0, err + } + if b == 0xff { + break + } + if b > 9 { + return 0, errInvalidPayload + } + ret = ret*10 + int64(b) + } + return ret, nil +} + +func readTextLen(r byteReader) (int64, error) { + ret := int64(0) + for { + b, err := r.ReadByte() + if err != nil { + return 0, err + } + if b == ':' { + break + } + if b < '0' || b > '9' { + return 0, errInvalidPayload + } + ret = ret*10 + int64(b-'0') + } + return ret, nil +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/server.go b/vendor/github.com/googollee/go-socket.io/engineio/server.go new file mode 100644 index 00000000000..625b1287aee --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/server.go @@ -0,0 +1,171 @@ +package engineio + +import ( + "context" + "fmt" + "io" + "log" + "net" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" + + "github.com/googollee/go-socket.io/engineio/session" + "github.com/googollee/go-socket.io/engineio/transport" +) + +// Server is instance of server +type Server struct { + pingInterval time.Duration + pingTimeout time.Duration + + transports *transport.Manager + sessions *session.Manager + + requestChecker CheckerFunc + connInitor ConnInitorFunc + + connChan chan Conn + closeOnce sync.Once +} + +// NewServer returns a server. +func NewServer(opts *Options) *Server { + return &Server{ + transports: transport.NewManager(opts.getTransport()), + pingInterval: opts.getPingInterval(), + pingTimeout: opts.getPingTimeout(), + requestChecker: opts.getRequestChecker(), + connInitor: opts.getConnInitor(), + sessions: session.NewManager(opts.getSessionIDGenerator()), + connChan: make(chan Conn, 1), + } +} + +// Close closes server. +func (s *Server) Close() error { + s.closeOnce.Do(func() { + close(s.connChan) + }) + return nil +} + +// Accept accepts a connection. +func (s *Server) Accept() (Conn, error) { + c := <-s.connChan + if c == nil { + return nil, io.EOF + } + return c, nil +} + +func (s *Server) Addr() net.Addr { + return nil +} + +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + + reqTransport := query.Get("transport") + srvTransport, ok := s.transports.Get(reqTransport) + if !ok || srvTransport == nil { + http.Error(w, fmt.Sprintf("invalid transport: %s", srvTransport), http.StatusBadRequest) + return + } + + header, err := s.requestChecker(r) + if err != nil { + http.Error(w, fmt.Sprintf("request checker err: %s", err.Error()), http.StatusBadGateway) + return + } + + for k, v := range header { + w.Header()[k] = v + } + + sid := query.Get("sid") + reqSession, ok := s.sessions.Get(sid) + // if we can't find session in current session pool, let's create this. behaviour for new connections + if !ok || reqSession == nil { + if sid != "" { + http.Error(w, fmt.Sprintf("invalid sid value: %s", sid), http.StatusBadRequest) + return + } + + transportConn, err := srvTransport.Accept(w, r) + if err != nil { + http.Error(w, fmt.Sprintf("transport accept err: %s", err.Error()), http.StatusBadGateway) + return + } + + reqSession, err = s.newSession(r.Context(), transportConn, reqTransport) + if err != nil { + http.Error(w, fmt.Sprintf("create new session err: %s", err.Error()), http.StatusBadRequest) + return + } + + s.connInitor(r, reqSession) + } + + // try upgrade current connection + if reqSession.Transport() != reqTransport { + transportConn, err := srvTransport.Accept(w, r) + if err != nil { + // don't call http.Error() for HandshakeErrors because + // they get handled by the websocket library internally. + if _, ok := err.(websocket.HandshakeError); !ok { + http.Error(w, err.Error(), http.StatusBadGateway) + } + return + } + + reqSession.Upgrade(reqTransport, transportConn) + + if handler, ok := transportConn.(http.Handler); ok { + handler.ServeHTTP(w, r) + } + return + } + + reqSession.ServeHTTP(w, r) +} + +// Count counts connected +func (s *Server) Count() int { + return s.sessions.Count() +} + +// Remove session from sessions pool. Experimental API. +func (s *Server) Remove(sid string) { + s.sessions.Remove(sid) +} + +func (s *Server) newSession(_ context.Context, conn transport.Conn, reqTransport string) (*session.Session, error) { + params := transport.ConnParameters{ + PingInterval: s.pingInterval, + PingTimeout: s.pingTimeout, + Upgrades: s.transports.UpgradeFrom(reqTransport), + } + + sid := s.sessions.NewID() + newSession, err := session.New(conn, sid, reqTransport, params) + if err != nil { + return nil, err + } + + go func(newSession *session.Session) { + if err = newSession.InitSession(); err != nil { + log.Println("init new session:", err) + + return + } + + s.sessions.Add(newSession) + + s.connChan <- newSession + }(newSession) + + return newSession, nil +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/server_options.go b/vendor/github.com/googollee/go-socket.io/engineio/server_options.go new file mode 100644 index 00000000000..493f6f2218e --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/server_options.go @@ -0,0 +1,74 @@ +package engineio + +import ( + "github.com/googollee/go-socket.io/engineio/session" + "net/http" + "time" + + "github.com/googollee/go-socket.io/engineio/transport" + "github.com/googollee/go-socket.io/engineio/transport/polling" + "github.com/googollee/go-socket.io/engineio/transport/websocket" +) + +// Options is options to create a server. +type Options struct { + PingTimeout time.Duration + PingInterval time.Duration + + Transports []transport.Transport + SessionIDGenerator session.IDGenerator + + RequestChecker CheckerFunc + ConnInitor ConnInitorFunc +} + +func (c *Options) getRequestChecker() CheckerFunc { + if c != nil && c.RequestChecker != nil { + return c.RequestChecker + } + return defaultChecker +} + +func (c *Options) getConnInitor() ConnInitorFunc { + if c != nil && c.ConnInitor != nil { + return c.ConnInitor + } + return defaultInitor +} + +func (c *Options) getPingTimeout() time.Duration { + if c != nil && c.PingTimeout != 0 { + return c.PingTimeout + } + return time.Minute +} + +func (c *Options) getPingInterval() time.Duration { + if c != nil && c.PingInterval != 0 { + return c.PingInterval + } + return time.Second * 20 +} + +func (c *Options) getTransport() []transport.Transport { + if c != nil && len(c.Transports) != 0 { + return c.Transports + } + return []transport.Transport{ + polling.Default, + websocket.Default, + } +} + +func (c *Options) getSessionIDGenerator() session.IDGenerator { + if c != nil && c.SessionIDGenerator != nil { + return c.SessionIDGenerator + } + return &session.DefaultIDGenerator{} +} + +func defaultChecker(*http.Request) (http.Header, error) { + return nil, nil +} + +func defaultInitor(*http.Request, Conn) {} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/session/base.go b/vendor/github.com/googollee/go-socket.io/engineio/session/base.go new file mode 100644 index 00000000000..37557147372 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/session/base.go @@ -0,0 +1,15 @@ +package session + +import ( + "github.com/googollee/go-socket.io/engineio/frame" +) + +// FrameType is type of a message frame. +type FrameType frame.Type + +const ( + // TEXT is text type message. + TEXT = FrameType(frame.String) + // BINARY is binary type message. + BINARY = FrameType(frame.Binary) +) diff --git a/vendor/github.com/googollee/go-socket.io/engineio/session/session.go b/vendor/github.com/googollee/go-socket.io/engineio/session/session.go new file mode 100644 index 00000000000..f8febd3bb9a --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/session/session.go @@ -0,0 +1,347 @@ +package session + +import ( + "io" + "net" + "net/http" + "net/url" + "sync" + "time" + + "github.com/googollee/go-socket.io/engineio/frame" + "github.com/googollee/go-socket.io/engineio/packet" + "github.com/googollee/go-socket.io/engineio/payload" + "github.com/googollee/go-socket.io/engineio/transport" +) + +// Pauser is connection which can be paused and resumes. +type Pauser interface { + Pause() + Resume() +} + +type Session struct { + conn transport.Conn + params transport.ConnParameters + transport string + + context interface{} + + upgradeLocker sync.RWMutex +} + +func New(conn transport.Conn, sid, transport string, params transport.ConnParameters) (*Session, error) { + params.SID = sid + + ses := &Session{ + transport: transport, + conn: conn, + params: params, + } + + if err := ses.setDeadline(); err != nil { + ses.Close() + return nil, err + } + + return ses, nil +} + +func (s *Session) SetContext(v interface{}) { + s.context = v +} + +func (s *Session) Context() interface{} { + return s.context +} + +func (s *Session) ID() string { + return s.params.SID +} + +func (s *Session) Transport() string { + s.upgradeLocker.RLock() + defer s.upgradeLocker.RUnlock() + + return s.transport +} + +func (s *Session) Close() error { + s.upgradeLocker.RLock() + defer s.upgradeLocker.RUnlock() + + return s.conn.Close() +} + +// NextReader attempts to obtain a ReadCloser from the session's connection. +// When finished writing, the caller MUST Close the ReadCloser to unlock the +// connection's FramerReader. +func (s *Session) NextReader() (FrameType, io.ReadCloser, error) { + for { + ft, pt, r, err := s.nextReader() + if err != nil { + s.Close() + return 0, nil, err + } + + switch pt { + case packet.PING: + // Respond to a ping with a pong. + err := func() error { + w, err := s.nextWriter(ft, packet.PONG) + if err != nil { + return err + } + // echo + _, err = io.Copy(w, r) + w.Close() // unlocks the wrapped connection's FrameWriter + r.Close() // unlocks the wrapped connection's FrameReader + return err + }() + if err != nil { + s.Close() + return 0, nil, err + } + // Read another frame. + if err := s.setDeadline(); err != nil { + s.Close() + return 0, nil, err + } + + case packet.CLOSE: + r.Close() // unlocks the wrapped connection's FrameReader + s.Close() + return 0, nil, io.EOF + + case packet.MESSAGE: + // Caller must Close the ReadCloser to unlock the connection's + // FrameReader when finished reading. + return FrameType(ft), r, nil + + default: + // Unknown packet type. Close reader and try again. + r.Close() + } + } +} + +func (s *Session) URL() url.URL { + s.upgradeLocker.RLock() + defer s.upgradeLocker.RUnlock() + + return s.conn.URL() +} + +func (s *Session) LocalAddr() net.Addr { + s.upgradeLocker.RLock() + defer s.upgradeLocker.RUnlock() + + return s.conn.LocalAddr() +} + +func (s *Session) RemoteAddr() net.Addr { + s.upgradeLocker.RLock() + defer s.upgradeLocker.RUnlock() + + return s.conn.RemoteAddr() +} + +func (s *Session) RemoteHeader() http.Header { + s.upgradeLocker.RLock() + defer s.upgradeLocker.RUnlock() + + return s.conn.RemoteHeader() +} + +// NextWriter attempts to obtain a WriteCloser from the session's connection. +// When finished writing, the caller MUST Close the WriteCloser to unlock the +// connection's FrameWriter. +func (s *Session) NextWriter(typ FrameType) (io.WriteCloser, error) { + return s.nextWriter(frame.Type(typ), packet.MESSAGE) +} + +func (s *Session) Upgrade(transport string, conn transport.Conn) { + go s.upgrading(transport, conn) +} + +func (s *Session) InitSession() error { + w, err := s.nextWriter(frame.String, packet.OPEN) + if err != nil { + s.Close() + return err + } + + if _, err := s.params.WriteTo(w); err != nil { + w.Close() + s.Close() + return err + } + + if err := w.Close(); err != nil { + s.Close() + return err + } + + return nil +} + +func (s *Session) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.upgradeLocker.RLock() + conn := s.conn + s.upgradeLocker.RUnlock() + + if h, ok := conn.(http.Handler); ok { + h.ServeHTTP(w, r) + } +} + +func (s *Session) nextReader() (frame.Type, packet.Type, io.ReadCloser, error) { + for { + s.upgradeLocker.RLock() + conn := s.conn + s.upgradeLocker.RUnlock() + + ft, pt, r, err := conn.NextReader() + if err != nil { + if op, ok := err.(payload.Error); ok && op.Temporary() { + continue + } + return 0, 0, nil, err + } + return ft, pt, r, nil + } +} + +func (s *Session) nextWriter(ft frame.Type, pt packet.Type) (io.WriteCloser, error) { + for { + s.upgradeLocker.RLock() + conn := s.conn + s.upgradeLocker.RUnlock() + + w, err := conn.NextWriter(ft, pt) + if err != nil { + if op, ok := err.(payload.Error); ok && op.Temporary() { + continue + } + return nil, err + } + // Caller must Close the WriteCloser to unlock the connection's + // FrameWriter when finished writing. + return w, nil + } +} + +func (s *Session) setDeadline() error { + s.upgradeLocker.RLock() + defer s.upgradeLocker.RUnlock() + + deadline := time.Now().Add(s.params.PingTimeout) + + err := s.conn.SetReadDeadline(deadline) + if err != nil { + return err + } + + return s.conn.SetWriteDeadline(deadline) +} + +func (s *Session) upgrading(t string, conn transport.Conn) { + // Read a ping from the client. + err := conn.SetReadDeadline(time.Now().Add(s.params.PingTimeout)) + if err != nil { + conn.Close() + return + } + + ft, pt, r, err := conn.NextReader() + if err != nil { + conn.Close() + return + } + if pt != packet.PING { + r.Close() + conn.Close() + return + } + // Wait to close the reader until after data is read and echoed in the reply. + + // Sent a pong in reply. + err = conn.SetWriteDeadline(time.Now().Add(s.params.PingTimeout)) + if err != nil { + r.Close() + conn.Close() + return + } + + w, err := conn.NextWriter(ft, packet.PONG) + if err != nil { + r.Close() + conn.Close() + return + } + // echo + if _, err = io.Copy(w, r); err != nil { + w.Close() + r.Close() + conn.Close() + return + } + if err = r.Close(); err != nil { + w.Close() + conn.Close() + return + } + if err = w.Close(); err != nil { + conn.Close() + return + } + + // Pause the old connection. + s.upgradeLocker.RLock() + old := s.conn + s.upgradeLocker.RUnlock() + + p, ok := old.(Pauser) + if !ok { + // old transport doesn't support upgrading + conn.Close() + return + } + + p.Pause() + + // Prepare to resume the connection if upgrade fails. + defer func() { + if p != nil { + p.Resume() + } + }() + + // Check for upgrade packet from the client. + _, pt, r, err = conn.NextReader() + if err != nil { + conn.Close() + return + } + + if pt != packet.UPGRADE { + r.Close() + conn.Close() + return + } + + if err = r.Close(); err != nil { + conn.Close() + return + } + + // Successful upgrade. + s.upgradeLocker.Lock() + s.conn = conn + s.transport = t + s.upgradeLocker.Unlock() + + p = nil + + old.Close() +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/session/session_id_generator.go b/vendor/github.com/googollee/go-socket.io/engineio/session/session_id_generator.go new file mode 100644 index 00000000000..b5e4c4403b0 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/session/session_id_generator.go @@ -0,0 +1,24 @@ +package session + +import ( + "strconv" + "sync/atomic" +) + +// IDGenerator generates new session id. Default behavior is simple +// increasing number. +// If you need custom session id, for example using local ip as prefix, you can +// implement SessionIDGenerator and save in Configure. Engine.io will use custom +// one to generate new session id. +type IDGenerator interface { + NewID() string +} + +type DefaultIDGenerator struct { + ID uint64 +} + +func (g *DefaultIDGenerator) NewID() string { + id := atomic.AddUint64(&g.ID, 1) + return strconv.FormatUint(id, 36) +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/session/session_manager.go b/vendor/github.com/googollee/go-socket.io/engineio/session/session_manager.go new file mode 100644 index 00000000000..493138f7c35 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/session/session_manager.go @@ -0,0 +1,54 @@ +package session + +import ( + "sync" +) + +type Manager struct { + IDGenerator + + sessions map[string]*Session + locker sync.RWMutex +} + +func NewManager(gen IDGenerator) *Manager { + if gen == nil { + gen = &DefaultIDGenerator{} + } + return &Manager{ + IDGenerator: gen, + sessions: make(map[string]*Session), + } +} + +func (m *Manager) Add(s *Session) { + m.locker.Lock() + defer m.locker.Unlock() + + m.sessions[s.ID()] = s +} + +func (m *Manager) Get(sid string) (*Session, bool) { + m.locker.RLock() + defer m.locker.RUnlock() + + s, ok := m.sessions[sid] + return s, ok +} + +func (m *Manager) Remove(sid string) { + m.locker.Lock() + defer m.locker.Unlock() + + if _, ok := m.sessions[sid]; !ok { + return + } + delete(m.sessions, sid) +} + +func (m *Manager) Count() int { + m.locker.Lock() + defer m.locker.Unlock() + + return len(m.sessions) +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/errors.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/errors.go new file mode 100644 index 00000000000..0a293b0ba9f --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/errors.go @@ -0,0 +1,6 @@ +package transport + +import "errors" + +// ErrInvalidFrame is returned when writing invalid frame type. +var ErrInvalidFrame = errors.New("invalid frame type") diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/manager.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/manager.go new file mode 100644 index 00000000000..69b78748a83 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/manager.go @@ -0,0 +1,51 @@ +package transport + +import ( + "net/http" + "net/url" +) + +// Transport is a transport which can creates base.Conn +type Transport interface { + Name() string + Accept(w http.ResponseWriter, r *http.Request) (Conn, error) + Dial(u *url.URL, requestHeader http.Header) (Conn, error) +} + +// Manager is a manager of transports. +type Manager struct { + order []string + transports map[string]Transport +} + +// NewManager creates a new manager. +func NewManager(transports []Transport) *Manager { + tranMap := make(map[string]Transport) + names := make([]string, len(transports)) + for i, t := range transports { + names[i] = t.Name() + tranMap[t.Name()] = t + } + + return &Manager{ + order: names, + transports: tranMap, + } +} + +// UpgradeFrom returns a name list of transports which can upgrade from given +// name. +func (m *Manager) UpgradeFrom(name string) []string { + for i, n := range m.order { + if n == name { + return m.order[i+1:] + } + } + return nil +} + +// Get returns the transport with given name. +func (m *Manager) Get(name string) (Transport, bool) { + t, ok := m.transports[name] + return t, ok +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/parameters.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/parameters.go new file mode 100644 index 00000000000..1f716400968 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/parameters.go @@ -0,0 +1,63 @@ +package transport + +import ( + "encoding/json" + "io" + "time" +) + +// ConnParameters is connection parameter of server. +type ConnParameters struct { + PingInterval time.Duration + PingTimeout time.Duration + SID string + Upgrades []string +} + +type jsonParameters struct { + SID string `json:"sid"` + Upgrades []string `json:"upgrades"` + PingInterval int `json:"pingInterval"` + PingTimeout int `json:"pingTimeout"` +} + +// ReadConnParameters reads ConnParameters from r. +func ReadConnParameters(r io.Reader) (ConnParameters, error) { + var param jsonParameters + if err := json.NewDecoder(r).Decode(¶m); err != nil { + return ConnParameters{}, err + } + + return ConnParameters{ + SID: param.SID, + Upgrades: param.Upgrades, + PingInterval: time.Duration(param.PingInterval) * time.Millisecond, + PingTimeout: time.Duration(param.PingTimeout) * time.Millisecond, + }, nil +} + +// WriteTo writes to w with json format. +func (p ConnParameters) WriteTo(w io.Writer) (int64, error) { + arg := jsonParameters{ + SID: p.SID, + Upgrades: p.Upgrades, + PingInterval: int(p.PingInterval / time.Millisecond), + PingTimeout: int(p.PingTimeout / time.Millisecond), + } + writer := writer{ + w: w, + } + err := json.NewEncoder(&writer).Encode(arg) + return writer.i, err +} + +type writer struct { + i int64 + w io.Writer +} + +func (w *writer) Write(p []byte) (int, error) { + n, err := w.w.Write(p) + w.i += int64(n) + return n, err +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/connect.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/connect.go new file mode 100644 index 00000000000..0aa611a82ef --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/connect.go @@ -0,0 +1,252 @@ +package polling + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "net/url" + "sync/atomic" + + "github.com/googollee/go-socket.io/engineio/packet" + "github.com/googollee/go-socket.io/engineio/payload" + "github.com/googollee/go-socket.io/engineio/transport" + "github.com/googollee/go-socket.io/engineio/transport/utils" +) + +type clientConn struct { + *payload.Payload + + httpClient *http.Client + request http.Request + remoteHeader atomic.Value +} + +func (c *clientConn) Open() (transport.ConnParameters, error) { + go c.getOpen() + + _, pt, r, err := c.NextReader() + if err != nil { + return transport.ConnParameters{}, err + } + + if pt != packet.OPEN { + r.Close() + return transport.ConnParameters{}, errors.New("invalid open") + } + + conn, err := transport.ReadConnParameters(r) + if err != nil { + r.Close() + return transport.ConnParameters{}, err + } + + err = r.Close() + + if err != nil { + return transport.ConnParameters{}, err + } + + query := c.request.URL.Query() + query.Set("sid", conn.SID) + c.request.URL.RawQuery = query.Encode() + + go c.serveGet() + go c.servePost() + + return conn, nil +} + +func (c *clientConn) URL() url.URL { + return *c.request.URL +} + +func (c *clientConn) LocalAddr() net.Addr { + return Addr{""} +} + +func (c *clientConn) RemoteAddr() net.Addr { + return Addr{c.request.Host} +} + +func (c *clientConn) RemoteHeader() http.Header { + ret := c.remoteHeader.Load() + if ret == nil { + return nil + } + return ret.(http.Header) +} + +func (c *clientConn) Resume() { + c.Payload.Resume() + + go c.serveGet() + go c.servePost() +} + +func (c *clientConn) servePost() { + req := c.request + reqUrl := *req.URL + + req.URL = &reqUrl + req.Method = http.MethodPost + + var buf bytes.Buffer + req.Body = ioutil.NopCloser(&buf) + + query := reqUrl.Query() + for { + buf.Reset() + + if err := c.Payload.FlushOut(&buf); err != nil { + return + } + query.Set("t", utils.Timestamp()) + req.URL.RawQuery = query.Encode() + + resp, err := c.httpClient.Do(&req) + if err != nil { + if err = c.Payload.Store("post", err); err != nil { + log.Println("store post error", err) + } + c.Close() + return + } + + discardBody(resp.Body) + + if resp.StatusCode != http.StatusOK { + err = c.Payload.Store("post", fmt.Errorf("invalid response: %s(%d)", resp.Status, resp.StatusCode)) + if err != nil { + log.Println("store post error", err) + } + c.Close() + return + } + + c.remoteHeader.Store(resp.Header) + } +} + +func (c *clientConn) getOpen() { + req := c.request + query := req.URL.Query() + + reqUrl := *req.URL + req.URL = &reqUrl + req.Method = http.MethodGet + + query.Set("t", utils.Timestamp()) + req.URL.RawQuery = query.Encode() + + resp, err := c.httpClient.Do(&req) + if err != nil { + if err = c.Payload.Store("get", err); err != nil { + log.Println("store get error", err) + } + + c.Close() + return + } + + defer func() { + discardBody(resp.Body) + }() + + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("invalid request: %s(%d)", resp.Status, resp.StatusCode) + } + + var isSupportBinary bool + if err == nil { + mime := resp.Header.Get("Content-Type") + isSupportBinary, err = mimeIsSupportBinary(mime) + if err != nil { + log.Println("check mime support binary", err) + } + } + + if err != nil { + if err = c.Payload.Store("get", err); err != nil { + log.Println("store get error", err) + } + c.Close() + + return + } + + c.remoteHeader.Store(resp.Header) + + if err = c.Payload.FeedIn(resp.Body, isSupportBinary); err != nil { + return + } +} + +func (c *clientConn) serveGet() { + req := c.request + reqUrl := *req.URL + + req.URL = &reqUrl + req.Method = http.MethodGet + + query := req.URL.Query() + for { + query.Set("t", utils.Timestamp()) + req.URL.RawQuery = query.Encode() + + resp, err := c.httpClient.Do(&req) + if err != nil { + if err = c.Payload.Store("get", err); err != nil { + log.Println("store get error", err) + } + c.Close() + + return + } + + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("invalid request: %s(%d)", resp.Status, resp.StatusCode) + } + + var isSupportBinary bool + if err == nil { + mime := resp.Header.Get("Content-Type") + isSupportBinary, err = mimeIsSupportBinary(mime) + if err != nil { + log.Println("check mime support binary", err) + } + } + + if err != nil { + discardBody(resp.Body) + + if err = c.Payload.Store("get", err); err != nil { + log.Println("store get error", err) + } + + c.Close() + + return + } + + if err = c.Payload.FeedIn(resp.Body, isSupportBinary); err != nil { + discardBody(resp.Body) + + return + } + + c.remoteHeader.Store(resp.Header) + } +} + +func discardBody(body io.ReadCloser) { + _, err := io.Copy(ioutil.Discard, body) + if err != nil { + log.Println("copy from body resp to discard", err) + } + body.Close() +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/server.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/server.go new file mode 100644 index 00000000000..f1af02c1aa4 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/server.go @@ -0,0 +1,147 @@ +package polling + +import ( + "bytes" + "fmt" + "html/template" + "net" + "net/http" + "net/url" + "strings" + + "github.com/googollee/go-socket.io/engineio/payload" +) + +type serverConn struct { + *payload.Payload + transport *Transport + supportBinary bool + + remoteHeader http.Header + localAddr Addr + remoteAddr Addr + url url.URL + jsonp string +} + +func newServerConn(t *Transport, r *http.Request) *serverConn { + query := r.URL.Query() + jsonp := query.Get("j") + supportBinary := query.Get("b64") == "" + if jsonp != "" { + supportBinary = false + } + + return &serverConn{ + Payload: payload.New(supportBinary), + transport: t, + supportBinary: supportBinary, + remoteHeader: r.Header, + localAddr: Addr{r.Host}, + remoteAddr: Addr{r.RemoteAddr}, + url: *r.URL, + jsonp: jsonp, + } +} + +func (c *serverConn) URL() url.URL { + return c.url +} + +func (c *serverConn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *serverConn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *serverConn) RemoteHeader() http.Header { + return c.remoteHeader +} + +func (c *serverConn) SetHeaders(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.UserAgent(), ";MSIE") || strings.Contains(r.UserAgent(), "Trident/") { + w.Header().Set("X-XSS-Protection", "0") + } + + // just in case the default behaviour gets changed and it has to handle an origin check + checkOrigin := Default.CheckOrigin + if c.transport.CheckOrigin != nil { + checkOrigin = c.transport.CheckOrigin + } + + if checkOrigin != nil && checkOrigin(r) { + if r.URL.Query().Get("j") == "" { + origin := r.Header.Get("Origin") + if origin == "" { + w.Header().Set("Access-Control-Allow-Origin", "*") + } else { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Credentials", "true") + } + } + } +} + +func (c *serverConn) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodOptions: + if r.URL.Query().Get("j") == "" { + c.SetHeaders(w, r) + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + w.WriteHeader(200) + } + + case http.MethodGet: + c.SetHeaders(w, r) + + if jsonp := r.URL.Query().Get("j"); jsonp != "" { + buf := bytes.NewBuffer(nil) + if err := c.Payload.FlushOut(buf); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/javascript; charset=UTF-8") + pl := template.JSEscapeString(buf.String()) + + _, _ = w.Write([]byte("___eio[" + jsonp + "](\"")) + _, _ = w.Write([]byte(pl)) + _, _ = w.Write([]byte("\");")) + + return + } + if c.supportBinary { + w.Header().Set("Content-Type", "application/octet-stream") + } else { + w.Header().Set("Content-Type", "text/plain; charset=UTF-8") + } + + if err := c.Payload.FlushOut(w); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + case http.MethodPost: + c.SetHeaders(w, r) + + mime := r.Header.Get("Content-Type") + isSupportBinary, err := mimeIsSupportBinary(mime) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if err := c.Payload.FeedIn(r.Body, isSupportBinary); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + _, err = w.Write([]byte("ok")) + if err != nil { + fmt.Printf("ack post err=%s\n", err.Error()) + } + + default: + http.Error(w, "invalid method", http.StatusBadRequest) + } +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/transport.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/transport.go new file mode 100644 index 00000000000..b6c30670c51 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/transport.go @@ -0,0 +1,74 @@ +package polling + +import ( + "net/http" + "net/url" + "time" + + "github.com/googollee/go-socket.io/engineio/payload" + "github.com/googollee/go-socket.io/engineio/transport" +) + +// Transport is the transport of polling. +type Transport struct { + Client *http.Client + CheckOrigin func(r *http.Request) bool +} + +// Default is the default transport. +var Default = &Transport{ + Client: &http.Client{ + Timeout: time.Minute, + }, + CheckOrigin: nil, +} + +// Name is the name of transport. +func (t *Transport) Name() string { + return "polling" +} + +// Accept accepts a http request and create Conn. +func (t *Transport) Accept(w http.ResponseWriter, r *http.Request) (transport.Conn, error) { + conn := newServerConn(t, r) + return conn, nil +} + +// Dial dials connection to url. +func (t *Transport) Dial(u *url.URL, requestHeader http.Header) (transport.Conn, error) { + query := u.Query() + query.Set("transport", t.Name()) + u.RawQuery = query.Encode() + + client := t.Client + if client == nil { + client = Default.Client + } + + return dial(client, u, requestHeader) +} + +func dial(client *http.Client, url *url.URL, requestHeader http.Header) (*clientConn, error) { + if client == nil { + client = &http.Client{} + } + req, err := http.NewRequest("", url.String(), nil) + if err != nil { + return nil, err + } + for k, v := range requestHeader { + req.Header[k] = v + } + supportBinary := req.URL.Query().Get("b64") == "" + if supportBinary { + req.Header.Set("Content-Type", "application/octet-stream") + } else { + req.Header.Set("Content-Type", "text/plain;charset=UTF-8") + } + + return &clientConn{ + Payload: payload.New(supportBinary), + httpClient: client, + request: *req, + }, nil +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/util.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/util.go new file mode 100644 index 00000000000..550c63b20e2 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/polling/util.go @@ -0,0 +1,40 @@ +package polling + +import ( + "errors" + "mime" + "strings" +) + +type Addr struct { + Host string +} + +func (a Addr) Network() string { + return "tcp" +} + +func (a Addr) String() string { + return a.Host +} + +func mimeIsSupportBinary(m string) (bool, error) { + typ, params, err := mime.ParseMediaType(m) + if err != nil { + return false, err + } + + switch typ { + case "application/octet-stream": + return true, nil + + case "text/plain": + charset := strings.ToLower(params["charset"]) + if charset != "utf-8" { + return false, errors.New("invalid charset") + } + return false, nil + } + + return false, errors.New("invalid content-type") +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/transport.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/transport.go new file mode 100644 index 00000000000..23e70c7af35 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/transport.go @@ -0,0 +1,35 @@ +package transport + +import ( + "io" + "net" + "net/http" + "net/url" + "time" + + "github.com/googollee/go-socket.io/engineio/frame" + "github.com/googollee/go-socket.io/engineio/packet" +) + +// FrameReader reads a frame. It needs be closed before next reading. +type FrameReader interface { + NextReader() (frame.Type, packet.Type, io.ReadCloser, error) +} + +// FrameWriter writes a frame. It needs be closed before next writing. +type FrameWriter interface { + NextWriter(ft frame.Type, pt packet.Type) (io.WriteCloser, error) +} + +// Conn is a transport connection. +type Conn interface { + FrameReader + FrameWriter + io.Closer + URL() url.URL + LocalAddr() net.Addr + RemoteAddr() net.Addr + RemoteHeader() http.Header + SetReadDeadline(t time.Time) error + SetWriteDeadline(t time.Time) error +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/utils/clock.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/utils/clock.go new file mode 100644 index 00000000000..e039aee4de9 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/utils/clock.go @@ -0,0 +1,30 @@ +package utils + +import "time" + +var chars = []byte("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_") + +type clock interface { + Now() time.Time +} + +type timeClock struct{} + +func (timeClock) Now() time.Time { + return time.Now() +} + +// Timestamp returns a string based on different nano time. +func Timestamp() string { + return TimestampFromClock(timeClock{}) +} + +func TimestampFromClock(c clock) string { + now := c.Now().UnixNano() + ret := make([]byte, 0, 16) + for now > 0 { + ret = append(ret, chars[int(now%int64(len(chars)))]) + now = now / int64(len(chars)) + } + return string(ret) +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/websocket/connect.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/websocket/connect.go new file mode 100644 index 00000000000..1715bfa51fe --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/websocket/connect.go @@ -0,0 +1,83 @@ +package websocket + +import ( + "net" + "net/http" + "net/url" + "sync" + "time" + + "github.com/gorilla/websocket" + + "github.com/googollee/go-socket.io/engineio/packet" + "github.com/googollee/go-socket.io/engineio/transport" +) + +// conn implements base.Conn +type conn struct { + transport.FrameReader + transport.FrameWriter + + ws wrapper + + url url.URL + remoteHeader http.Header + + closed chan struct{} + closeOnce sync.Once +} + +func newConn(ws *websocket.Conn, url url.URL, header http.Header) *conn { + w := newWrapper(ws) + closed := make(chan struct{}) + + return &conn{ + url: url, + remoteHeader: header, + ws: w, + closed: closed, + FrameReader: packet.NewDecoder(w), + FrameWriter: packet.NewEncoder(w), + } +} + +func (c *conn) URL() url.URL { + return c.url +} + +func (c *conn) RemoteHeader() http.Header { + return c.remoteHeader +} + +func (c *conn) LocalAddr() net.Addr { + return c.ws.LocalAddr() +} + +func (c *conn) RemoteAddr() net.Addr { + return c.ws.RemoteAddr() +} + +func (c *conn) SetReadDeadline(t time.Time) error { + return c.ws.SetReadDeadline(t) +} + +func (c *conn) SetWriteDeadline(t time.Time) error { + // TODO: is locking really needed for SetWriteDeadline? If so, what about + // the read deadline? + c.ws.writeLocker.Lock() + err := c.ws.SetWriteDeadline(t) + c.ws.writeLocker.Unlock() + + return err +} + +func (c *conn) ServeHTTP(w http.ResponseWriter, r *http.Request) { + <-c.closed +} + +func (c *conn) Close() error { + c.closeOnce.Do(func() { + close(c.closed) + }) + return c.ws.Close() +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/websocket/transport.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/websocket/transport.go new file mode 100644 index 00000000000..408d5397363 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/websocket/transport.go @@ -0,0 +1,94 @@ +package websocket + +import ( + "crypto/tls" + "net" + "net/http" + "net/url" + "time" + + "github.com/gorilla/websocket" + + "github.com/googollee/go-socket.io/engineio/transport" + "github.com/googollee/go-socket.io/engineio/transport/utils" +) + +// DialError is the error when dialing to a server. It saves Response from +// server. +type DialError struct { + Response *http.Response + + error +} + +// Transport is websocket transport. +type Transport struct { + ReadBufferSize int + WriteBufferSize int + + Subprotocols []string + TLSClientConfig *tls.Config + HandshakeTimeout time.Duration + + Proxy func(*http.Request) (*url.URL, error) + NetDial func(network, addr string) (net.Conn, error) + CheckOrigin func(r *http.Request) bool +} + +// Default is default transport. +var Default = &Transport{} + +// Name is the name of websocket transport. +func (t *Transport) Name() string { + return "websocket" +} + +// Dial creates a new client connection. +func (t *Transport) Dial(u *url.URL, requestHeader http.Header) (transport.Conn, error) { + dialer := websocket.Dialer{ + ReadBufferSize: t.ReadBufferSize, + WriteBufferSize: t.WriteBufferSize, + NetDial: t.NetDial, + Proxy: t.Proxy, + TLSClientConfig: t.TLSClientConfig, + HandshakeTimeout: t.HandshakeTimeout, + Subprotocols: t.Subprotocols, + } + + switch u.Scheme { + case "http": + u.Scheme = "ws" + case "https": + u.Scheme = "wss" + } + + query := u.Query() + query.Set("transport", t.Name()) + query.Set("t", utils.Timestamp()) + + u.RawQuery = query.Encode() + c, resp, err := dialer.Dial(u.String(), requestHeader) + if err != nil { + return nil, DialError{ + error: err, + Response: resp, + } + } + + return newConn(c, *u, resp.Header), nil +} + +// Accept accepts a http request and create Conn. +func (t *Transport) Accept(w http.ResponseWriter, r *http.Request) (transport.Conn, error) { + upgrader := websocket.Upgrader{ + ReadBufferSize: t.ReadBufferSize, + WriteBufferSize: t.WriteBufferSize, + CheckOrigin: t.CheckOrigin, + } + c, err := upgrader.Upgrade(w, r, w.Header()) + if err != nil { + return nil, err + } + + return newConn(c, *r.URL, r.Header), nil +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/transport/websocket/wrapper.go b/vendor/github.com/googollee/go-socket.io/engineio/transport/websocket/wrapper.go new file mode 100644 index 00000000000..ddc13f40110 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/transport/websocket/wrapper.go @@ -0,0 +1,150 @@ +package websocket + +import ( + "fmt" + "io" + "io/ioutil" + "sync" + "time" + + "github.com/gorilla/websocket" + + "github.com/googollee/go-socket.io/engineio/frame" + "github.com/googollee/go-socket.io/engineio/transport" +) + +type wrapper struct { + *websocket.Conn + + writeLocker *sync.Mutex + readLocker *sync.Mutex +} + +func newWrapper(conn *websocket.Conn) wrapper { + return wrapper{ + Conn: conn, + writeLocker: new(sync.Mutex), + readLocker: new(sync.Mutex), + } +} + +func (w wrapper) NextReader() (frame.Type, io.ReadCloser, error) { + w.readLocker.Lock() + defer w.readLocker.Unlock() + + // The wrapper remains locked until the returned ReadCloser is Closed. + typ, r, err := w.Conn.NextReader() + if err != nil { + return 0, nil, err + } + + switch typ { + case websocket.TextMessage: + return frame.String, newRcWrapper(w.readLocker, r), nil + case websocket.BinaryMessage: + return frame.Binary, newRcWrapper(w.readLocker, r), nil + } + + return 0, nil, transport.ErrInvalidFrame +} + +type rcWrapper struct { + io.Reader + nagTimer *time.Timer + quitNag chan struct{} + l *sync.Mutex +} + +func newRcWrapper(l *sync.Mutex, r io.Reader) rcWrapper { + timer := time.NewTimer(30 * time.Second) + q := make(chan struct{}) + + go func() { + select { + case <-q: + case <-timer.C: + fmt.Println("Did you forget to Close() the ReadCloser from NextReader?") + } + }() + + return rcWrapper{ + nagTimer: timer, + quitNag: q, + l: l, + Reader: r, + } +} + +func (r rcWrapper) Close() error { + // Stop the nagger. + r.l.Lock() + defer r.l.Unlock() + + r.nagTimer.Stop() + close(r.quitNag) + + // Attempt to drain the Reader. + _, err := io.Copy(ioutil.Discard, r) + + return err +} + +func (w wrapper) NextWriter(FType frame.Type) (io.WriteCloser, error) { + var t int + + switch FType { + case frame.String: + t = websocket.TextMessage + case frame.Binary: + t = websocket.BinaryMessage + default: + return nil, transport.ErrInvalidFrame + } + + w.writeLocker.Lock() + writer, err := w.Conn.NextWriter(t) + // The wrapper remains locked until the returned WriteCloser is Closed. + if err != nil { + w.writeLocker.Unlock() + return nil, err + } + + return newWcWrapper(w.writeLocker, writer), nil +} + +type wcWrapper struct { + io.WriteCloser + nagTimer *time.Timer + + l *sync.Mutex + quitNag chan struct{} +} + +func newWcWrapper(l *sync.Mutex, w io.WriteCloser) wcWrapper { + timer := time.NewTimer(30 * time.Second) + chQuit := make(chan struct{}) + + go func() { + select { + case <-chQuit: + case <-timer.C: + fmt.Println("Did you forget to Close() the WriteCloser from NextWriter?") + } + }() + + return wcWrapper{ + nagTimer: timer, + quitNag: chQuit, + l: l, + WriteCloser: w, + } +} + +func (w wcWrapper) Close() error { + // Stop the nagger. + w.nagTimer.Stop() + close(w.quitNag) + // Unlock the wrapper's write lock for future calls to NextWriter. + defer w.l.Unlock() + return w.WriteCloser.Close() +} diff --git a/vendor/github.com/googollee/go-socket.io/engineio/types.go b/vendor/github.com/googollee/go-socket.io/engineio/types.go new file mode 100644 index 00000000000..6ca37102766 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/engineio/types.go @@ -0,0 +1,11 @@ +package engineio + +import ( + "net/http" +) + +// CheckerFunc is function to check request. +type CheckerFunc func(*http.Request) (http.Header, error) + +// ConnInitorFunc is function to do after create connection. +type ConnInitorFunc func(*http.Request, Conn) diff --git a/vendor/github.com/googollee/go-socket.io/errors.go b/vendor/github.com/googollee/go-socket.io/errors.go new file mode 100644 index 00000000000..78c1764fe0e --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/errors.go @@ -0,0 +1,37 @@ +package socketio + +import ( + "errors" + "fmt" +) + +// connect errors. +var ( + errUnavailableRootHandler = errors.New("root ('/') doesn't have a namespace handler") + + errFailedConnectNamespace = errors.New("failed connect to namespace without handler") +) + +// common connection dispatch errors. +var ( + errHandleDispatch = errors.New("handler dispatch error") + + errDecodeArgs = errors.New("decode args error") +) + +type errorMessage struct { + namespace string + + err error +} + +func (e errorMessage) Error() string { + return fmt.Sprintf("error in namespace: (%s) with error: (%s)", e.namespace, e.err.Error()) +} + +func newErrorMessage(namespace string, err error) *errorMessage { + return &errorMessage{ + namespace: namespace, + err: err, + } +} diff --git a/vendor/github.com/googollee/go-socket.io/handler.go b/vendor/github.com/googollee/go-socket.io/handler.go index b6a689d5572..03588686921 100644 --- a/vendor/github.com/googollee/go-socket.io/handler.go +++ b/vendor/github.com/googollee/go-socket.io/handler.go @@ -3,217 +3,79 @@ package socketio import ( "fmt" "reflect" - "sync" ) -type baseHandler struct { - events map[string]*caller - name string - broadcast BroadcastAdaptor - evMu sync.Mutex -} +const ( + goSocketIOConnInterface = "Conn" +) -func newBaseHandler(name string, broadcast BroadcastAdaptor) *baseHandler { - return &baseHandler{ - events: make(map[string]*caller), - name: name, - broadcast: broadcast, - evMu: sync.Mutex{}, - } +type funcHandler struct { + argTypes []reflect.Type + f reflect.Value } -// On registers the function f to handle an event. -func (h *baseHandler) On(event string, f interface{}) error { - c, err := newCaller(f) - if err != nil { - return err - } - h.evMu.Lock() - h.events[event] = c - h.evMu.Unlock() - return nil -} +func (h *funcHandler) Call(args []reflect.Value) (ret []reflect.Value, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = fmt.Errorf("event call error: %s", r) + } + } + }() -type socketHandler struct { - *baseHandler - acksmu sync.Mutex - acks map[int]*caller - socket *socket - rooms map[string]struct{} -} + ret = h.f.Call(args) -func newSocketHandler(s *socket, base *baseHandler) *socketHandler { - events := make(map[string]*caller) - base.evMu.Lock() - for k, v := range base.events { - events[k] = v - } - base.evMu.Unlock() - return &socketHandler{ - baseHandler: &baseHandler{ - events: events, - broadcast: base.broadcast, - evMu: base.evMu, - }, - acks: make(map[int]*caller), - socket: s, - rooms: make(map[string]struct{}), - } + return } -func (h *socketHandler) Emit(event string, args ...interface{}) error { - var c *caller - if l := len(args); l > 0 { - fv := reflect.ValueOf(args[l-1]) - if fv.Kind() == reflect.Func { - var err error - c, err = newCaller(args[l-1]) - if err != nil { - return err - } - args = args[:l-1] - } - } - args = append([]interface{}{event}, args...) - if c != nil { - id, err := h.socket.sendId(args) - if err != nil { - return err - } - h.acksmu.Lock() - h.acks[id] = c - h.acksmu.Unlock() - return nil - } - return h.socket.send(args) -} +func newEventFunc(f interface{}) *funcHandler { + fv := reflect.ValueOf(f) -func (h *socketHandler) Rooms() []string { - ret := make([]string, len(h.rooms)) - i := 0 - for room := range h.rooms { - ret[i] = room - i++ + if fv.Kind() != reflect.Func { + panic("event handler must be a func.") } - return ret -} + ft := fv.Type() -func (h *socketHandler) Join(room string) error { - if err := h.baseHandler.broadcast.Join(h.broadcastName(room), h.socket); err != nil { - return err + if ft.NumIn() < 1 || ft.In(0).Name() != goSocketIOConnInterface { + panic("handler function should be like func(socketio.Conn, ...)") } - h.rooms[room] = struct{}{} - return nil -} -func (h *socketHandler) Leave(room string) error { - if err := h.baseHandler.broadcast.Leave(h.broadcastName(room), h.socket); err != nil { - return err + argTypes := make([]reflect.Type, ft.NumIn()-1) + for i := range argTypes { + argTypes[i] = ft.In(i + 1) } - delete(h.rooms, room) - return nil -} -func (h *socketHandler) LeaveAll() error { - for room := range h.rooms { - if err := h.baseHandler.broadcast.Leave(h.broadcastName(room), h.socket); err != nil { - return err - } + if len(argTypes) == 0 { + argTypes = nil } - return nil -} - -func (h *baseHandler) BroadcastTo(room, event string, args ...interface{}) error { - return h.broadcast.Send(nil, h.broadcastName(room), event, args...) -} - -func (h *socketHandler) BroadcastTo(room, event string, args ...interface{}) error { - return h.baseHandler.broadcast.Send(h.socket, h.broadcastName(room), event, args...) -} -func (h *baseHandler) broadcastName(room string) string { - return fmt.Sprintf("%s:%s", h.name, room) + return &funcHandler{ + argTypes: argTypes, + f: fv, + } } -func (h *socketHandler) onPacket(decoder *decoder, packet *packet) ([]interface{}, error) { - defer func() { - if decoder != nil { - decoder.Close() - } - }() +func newAckFunc(f interface{}) *funcHandler { + fv := reflect.ValueOf(f) - var message string - switch packet.Type { - case _CONNECT: - message = "connection" - case _DISCONNECT: - message = "disconnection" - case _ERROR: - message = "error" - case _ACK: - fallthrough - case _BINARY_ACK: - return nil, h.onAck(packet.Id, decoder, packet) - default: - if decoder != nil { - message = decoder.Message() - } - } - h.evMu.Lock() - c, ok := h.events[message] - h.evMu.Unlock() - if !ok { - // If the message is not recognized by the server, the decoder.currentCloser - // needs to be closed otherwise the server will be stuck until the e - if decoder != nil { - decoder.Close() - } - return nil, nil - } - args := c.GetArgs() - olen := len(args) - if olen > 0 && decoder != nil { - packet.Data = &args - if err := decoder.DecodeData(packet); err != nil { - return nil, err - } - } - for i := len(args); i < olen; i++ { - args = append(args, nil) + if fv.Kind() != reflect.Func { + panic("ack callback must be a func.") } - retV := c.Call(h.socket, args) - if len(retV) == 0 { - return nil, nil - } + ft := fv.Type() + argTypes := make([]reflect.Type, ft.NumIn()) - var err error - if last, ok := retV[len(retV)-1].Interface().(error); ok { - err = last - retV = retV[0 : len(retV)-1] - } - ret := make([]interface{}, len(retV)) - for i, v := range retV { - ret[i] = v.Interface() + for i := range argTypes { + argTypes[i] = ft.In(i) } - return ret, err -} - -func (h *socketHandler) onAck(id int, decoder *decoder, packet *packet) error { - h.acksmu.Lock() - c, ok := h.acks[id] - if !ok { - h.acksmu.Unlock() - return nil + if len(argTypes) == 0 { + argTypes = nil } - delete(h.acks, id) - h.acksmu.Unlock() - args := c.GetArgs() - packet.Data = &args - if err := decoder.DecodeData(packet); err != nil { - return err + return &funcHandler{ + argTypes: argTypes, + f: fv, } - c.Call(h.socket, args) - return nil } diff --git a/vendor/github.com/googollee/go-socket.io/helpers.go b/vendor/github.com/googollee/go-socket.io/helpers.go new file mode 100644 index 00000000000..8348f2cb540 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/helpers.go @@ -0,0 +1,7 @@ +package socketio + +import "github.com/gofrs/uuid" + +func newV4UUID() string { + return uuid.Must(uuid.NewV4()).String() +} diff --git a/vendor/github.com/googollee/go-socket.io/ioutil.go b/vendor/github.com/googollee/go-socket.io/ioutil.go deleted file mode 100644 index 7767d4e4081..00000000000 --- a/vendor/github.com/googollee/go-socket.io/ioutil.go +++ /dev/null @@ -1,34 +0,0 @@ -package socketio - -import ( - "io" -) - -type writerHelper struct { - writer io.Writer - err error -} - -func newWriterHelper(w io.Writer) *writerHelper { - return &writerHelper{ - writer: w, - } -} - -func (h *writerHelper) Write(p []byte) { - if h.err != nil { - return - } - for len(p) > 0 { - n, err := h.writer.Write(p) - if err != nil { - h.err = err - return - } - p = p[n:] - } -} - -func (h *writerHelper) Error() error { - return h.err -} diff --git a/vendor/github.com/googollee/go-socket.io/main.go b/vendor/github.com/googollee/go-socket.io/main.go deleted file mode 100644 index f01ed76d044..00000000000 --- a/vendor/github.com/googollee/go-socket.io/main.go +++ /dev/null @@ -1,6 +0,0 @@ -/* -go-socket.io is a server implementation of socket.io in golang. - -It is compatible with the official Node.js implementation. -*/ -package socketio diff --git a/vendor/github.com/googollee/go-socket.io/message_reader.go b/vendor/github.com/googollee/go-socket.io/message_reader.go deleted file mode 100644 index 9feb0b4040e..00000000000 --- a/vendor/github.com/googollee/go-socket.io/message_reader.go +++ /dev/null @@ -1,60 +0,0 @@ -package socketio - -import ( - "bufio" -) - -type messageReader struct { - reader *bufio.Reader - message string - firstRead bool -} - -func newMessageReader(bufr *bufio.Reader) (*messageReader, error) { - if _, err := bufr.ReadBytes('"'); err != nil { - return nil, err - } - msg, err := bufr.ReadBytes('"') - if err != nil { - return nil, err - } - for { - b, err := bufr.Peek(1) - if err != nil { - return nil, err - } - if b[0] == ',' { - bufr.ReadByte() - break - } - if b[0] != ' ' { - break - } - bufr.ReadByte() - } - return &messageReader{ - reader: bufr, - message: string(msg[:len(msg)-1]), - firstRead: true, - }, nil -} - -func (r *messageReader) Message() string { - return r.message -} - -func (r *messageReader) Read(b []byte) (int, error) { - if len(b) == 0 { - return 0, nil - } - if r.firstRead { - r.firstRead = false - b[0] = '[' - n, err := r.reader.Read(b[1:]) - if err != nil { - return -1, err - } - return n + 1, err - } - return r.reader.Read(b) -} diff --git a/vendor/github.com/googollee/go-socket.io/namespace.go b/vendor/github.com/googollee/go-socket.io/namespace.go deleted file mode 100644 index f2e1004e31e..00000000000 --- a/vendor/github.com/googollee/go-socket.io/namespace.go +++ /dev/null @@ -1,47 +0,0 @@ -package socketio - -// Namespace is the name space of a socket.io handler. -type Namespace interface { - - // Name returns the name of the namespace. - Name() string - - // Of returns the namespace with given name. - Of(name string) Namespace - - // On registers the function f to handle an event. - On(event string, f interface{}) error -} - -type namespace struct { - *baseHandler - root map[string]Namespace -} - -func newNamespace(broadcast BroadcastAdaptor) *namespace { - ret := &namespace{ - baseHandler: newBaseHandler("", broadcast), - root: make(map[string]Namespace), - } - ret.root[ret.Name()] = ret - return ret -} - -func (n *namespace) Name() string { - return n.baseHandler.name -} - -func (n *namespace) Of(name string) Namespace { - if name == "/" { - name = "" - } - if ret, ok := n.root[name]; ok { - return ret - } - ret := &namespace{ - baseHandler: newBaseHandler(name, n.baseHandler.broadcast), - root: n.root, - } - n.root[name] = ret - return ret -} diff --git a/vendor/github.com/googollee/go-socket.io/namespace_conn.go b/vendor/github.com/googollee/go-socket.io/namespace_conn.go new file mode 100644 index 00000000000..7d024d0aa3f --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/namespace_conn.go @@ -0,0 +1,135 @@ +package socketio + +import ( + "fmt" + "reflect" + "sync" + + "github.com/googollee/go-socket.io/parser" +) + +// Namespace describes a communication channel that allows you to split the logic of your application +// over a single shared connection. +type Namespace interface { + // Context of this connection. You can save one context for one + // connection, and share it between all handlers. The handlers + // are called in one goroutine, so no need to lock context if it + // only accessed in one connection. + Context() interface{} + SetContext(ctx interface{}) + + Namespace() string + Emit(eventName string, v ...interface{}) + + Join(room string) + Leave(room string) + LeaveAll() + Rooms() []string +} + +type namespaceConn struct { + *conn + broadcast Broadcast + + namespace string + context interface{} + + ack sync.Map +} + +func newNamespaceConn(conn *conn, namespace string, broadcast Broadcast) *namespaceConn { + return &namespaceConn{ + conn: conn, + namespace: namespace, + broadcast: broadcast, + } +} + +func (nc *namespaceConn) SetContext(ctx interface{}) { + nc.context = ctx +} + +func (nc *namespaceConn) Context() interface{} { + return nc.context +} + +func (nc *namespaceConn) Namespace() string { + return nc.namespace +} + +func (nc *namespaceConn) Emit(eventName string, v ...interface{}) { + header := parser.Header{ + Type: parser.Event, + } + + if nc.namespace != aliasRootNamespace { + header.Namespace = nc.namespace + } + + if l := len(v); l > 0 { + last := v[l-1] + lastV := reflect.TypeOf(last) + + if lastV.Kind() == reflect.Func { + f := newAckFunc(last) + + header.ID = nc.conn.nextID() + header.NeedAck = true + + nc.ack.Store(header.ID, f) + v = v[:l-1] + } + } + + args := make([]reflect.Value, len(v)+1) + args[0] = reflect.ValueOf(eventName) + + for i := 1; i < len(args); i++ { + args[i] = reflect.ValueOf(v[i-1]) + } + + nc.conn.write(header, args...) +} + +func (nc *namespaceConn) Join(room string) { + nc.broadcast.Join(room, nc) +} + +func (nc *namespaceConn) Leave(room string) { + nc.broadcast.Leave(room, nc) +} + +func (nc *namespaceConn) LeaveAll() { + nc.broadcast.LeaveAll(nc) +} + +func (nc *namespaceConn) Rooms() []string { + return nc.broadcast.Rooms(nc) +} + +func (nc *namespaceConn) dispatch(header parser.Header) { + if header.Type != parser.Ack { + return + } + + rawFunc, ok := nc.ack.Load(header.ID) + if ok { + f, ok := rawFunc.(*funcHandler) + if !ok { + nc.conn.onError(nc.namespace, fmt.Errorf("incorrect data stored for header %d", header.ID)) + return + } + + nc.ack.Delete(header.ID) + + args, err := nc.conn.parseArgs(f.argTypes) + if err != nil { + nc.conn.onError(nc.namespace, err) + return + } + if _, err := f.Call(args); err != nil { + nc.conn.onError(nc.namespace, err) + return + } + } +} diff --git a/vendor/github.com/googollee/go-socket.io/namespace_handler.go b/vendor/github.com/googollee/go-socket.io/namespace_handler.go new file mode 100644 index 00000000000..3758a8aa272 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/namespace_handler.go @@ -0,0 +1,113 @@ +package socketio + +import ( + "errors" + "reflect" + "sync" + + "github.com/googollee/go-socket.io/parser" +) + +type namespaceHandler struct { + broadcast Broadcast + + events map[string]*funcHandler + eventsLock sync.RWMutex + + onConnect func(conn Conn) error + onDisconnect func(conn Conn, msg string) + onError func(conn Conn, err error) +} + +func newNamespaceHandler(nsp string, adapterOpts *RedisAdapterOptions) *namespaceHandler { + var broadcast Broadcast + if adapterOpts == nil { + broadcast = newBroadcast() + } else { + broadcast, _ = newRedisBroadcast(nsp, adapterOpts) + } + + return &namespaceHandler{ + broadcast: broadcast, + events: make(map[string]*funcHandler), + } +} + +func (nh *namespaceHandler) OnConnect(f func(Conn) error) { + nh.onConnect = f +} + +func (nh *namespaceHandler) OnDisconnect(f func(Conn, string)) { + nh.onDisconnect = f +} + +func (nh *namespaceHandler) OnError(f func(Conn, error)) { + nh.onError = f +} + +func (nh *namespaceHandler) OnEvent(event string, f interface{}) { + nh.eventsLock.Lock() + defer nh.eventsLock.Unlock() + + nh.events[event] = newEventFunc(f) +} + +func (nh *namespaceHandler) getEventTypes(event string) []reflect.Type { + nh.eventsLock.RLock() + namespaceHandler := nh.events[event] + nh.eventsLock.RUnlock() + + if namespaceHandler != nil { + return namespaceHandler.argTypes + } + + return nil +} + +func (nh *namespaceHandler) dispatch(conn Conn, header parser.Header, args ...reflect.Value) ([]reflect.Value, error) { + switch header.Type { + case parser.Connect: + if nh.onConnect != nil { + return nil, nh.onConnect(conn) + } + return nil, nil + + case parser.Disconnect: + if nh.onDisconnect != nil { + nh.onDisconnect(conn, getDispatchMessage(args...)) + } + return nil, nil + + case parser.Error: + if nh.onError != nil { + msg := getDispatchMessage(args...) + if msg == "" { + msg = "parser error dispatch" + } + nh.onError(conn, errors.New(msg)) + } + } + + return nil, parser.ErrInvalidPacketType +} + +func (nh *namespaceHandler) dispatchEvent(conn Conn, event string, args ...reflect.Value) ([]reflect.Value, error) { + nh.eventsLock.RLock() + namespaceHandler := nh.events[event] + nh.eventsLock.RUnlock() + + if namespaceHandler == nil { + return nil, nil + } + + return namespaceHandler.Call(append([]reflect.Value{reflect.ValueOf(conn)}, args...)) +} + +func getDispatchMessage(args ...reflect.Value) string { + var msg string + if len(args) > 0 { + msg = args[0].Interface().(string) + } + + return msg +} diff --git a/vendor/github.com/googollee/go-socket.io/namespace_handlers.go b/vendor/github.com/googollee/go-socket.io/namespace_handlers.go new file mode 100644 index 00000000000..6820461cd9c --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/namespace_handlers.go @@ -0,0 +1,29 @@ +package socketio + +import "sync" + +type namespaceHandlers struct { + handlers map[string]*namespaceHandler + mu sync.RWMutex +} + +func newNamespaceHandlers() *namespaceHandlers { + return &namespaceHandlers{ + handlers: make(map[string]*namespaceHandler), + } +} + +func (h *namespaceHandlers) Set(namespace string, handler *namespaceHandler) { + h.mu.Lock() + defer h.mu.Unlock() + + h.handlers[namespace] = handler +} + +func (h *namespaceHandlers) Get(nsp string) (*namespaceHandler, bool) { + h.mu.RLock() + defer h.mu.RUnlock() + + handler, ok := h.handlers[nsp] + return handler, ok +} diff --git a/vendor/github.com/googollee/go-socket.io/namespaces.go b/vendor/github.com/googollee/go-socket.io/namespaces.go new file mode 100644 index 00000000000..ac9d242314f --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/namespaces.go @@ -0,0 +1,45 @@ +package socketio + +import "sync" + +type namespaces struct { + namespaces map[string]*namespaceConn + mu sync.RWMutex +} + +func newNamespaces() *namespaces { + return &namespaces{ + namespaces: make(map[string]*namespaceConn), + } +} + +func (n *namespaces) Get(ns string) (*namespaceConn, bool) { + n.mu.RLock() + defer n.mu.RUnlock() + + namespace, ok := n.namespaces[ns] + return namespace, ok +} + +func (n *namespaces) Set(ns string, conn *namespaceConn) { + n.mu.Lock() + defer n.mu.Unlock() + + n.namespaces[ns] = conn +} + +func (n *namespaces) Delete(ns string) { + n.mu.Lock() + defer n.mu.Unlock() + + delete(n.namespaces, ns) +} + +func (n *namespaces) Range(fn func(ns string, nc *namespaceConn)) { + n.mu.RLock() + defer n.mu.RUnlock() + + for ns, nc := range n.namespaces { + fn(ns, nc) + } +} diff --git a/vendor/github.com/googollee/go-socket.io/parser.go b/vendor/github.com/googollee/go-socket.io/parser.go deleted file mode 100644 index 8555e6d612c..00000000000 --- a/vendor/github.com/googollee/go-socket.io/parser.go +++ /dev/null @@ -1,336 +0,0 @@ -package socketio - -import ( - "bufio" - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "strconv" - - "github.com/googollee/go-engine.io" -) - -const Protocol = 4 - -type packetType int - -const ( - _CONNECT packetType = iota - _DISCONNECT - _EVENT - _ACK - _ERROR - _BINARY_EVENT - _BINARY_ACK -) - -func (t packetType) String() string { - switch t { - case _CONNECT: - return "connect" - case _DISCONNECT: - return "disconnect" - case _EVENT: - return "event" - case _ACK: - return "ack" - case _ERROR: - return "error" - case _BINARY_EVENT: - return "binary_event" - case _BINARY_ACK: - return "binary_ack" - } - return fmt.Sprintf("unknown(%d)", t) -} - -type frameReader interface { - NextReader() (engineio.MessageType, io.ReadCloser, error) -} - -type frameWriter interface { - NextWriter(engineio.MessageType) (io.WriteCloser, error) -} - -type packet struct { - Type packetType - NSP string - Id int - Data interface{} - attachNumber int -} - -type encoder struct { - w frameWriter - err error -} - -func newEncoder(w frameWriter) *encoder { - return &encoder{ - w: w, - } -} - -func (e *encoder) Encode(v packet) error { - attachments := encodeAttachments(v.Data) - v.attachNumber = len(attachments) - if v.attachNumber > 0 { - v.Type += _BINARY_EVENT - _EVENT - } - if err := e.encodePacket(v); err != nil { - return err - } - for _, a := range attachments { - if err := e.writeBinary(a); err != nil { - return err - } - } - return nil -} - -func (e *encoder) encodePacket(v packet) error { - writer, err := e.w.NextWriter(engineio.MessageText) - if err != nil { - return err - } - defer writer.Close() - - w := newTrimWriter(writer, "\n") - wh := newWriterHelper(w) - wh.Write([]byte{byte(v.Type) + '0'}) - if v.Type == _BINARY_EVENT || v.Type == _BINARY_ACK { - wh.Write([]byte(fmt.Sprintf("%d-", v.attachNumber))) - } - needEnd := false - if v.NSP != "" { - wh.Write([]byte(v.NSP)) - needEnd = true - } - if v.Id >= 0 { - f := "%d" - if needEnd { - f = ",%d" - needEnd = false - } - wh.Write([]byte(fmt.Sprintf(f, v.Id))) - } - if v.Data != nil { - if needEnd { - wh.Write([]byte{','}) - needEnd = false - } - if wh.Error() != nil { - return wh.Error() - } - encoder := json.NewEncoder(w) - return encoder.Encode(v.Data) - } - return wh.Error() -} - -func (e *encoder) writeBinary(r io.Reader) error { - writer, err := e.w.NextWriter(engineio.MessageBinary) - if err != nil { - return err - } - defer writer.Close() - - if _, err := io.Copy(writer, r); err != nil { - return err - } - return nil - -} - -type decoder struct { - reader frameReader - message string - current io.Reader - currentCloser io.Closer -} - -func newDecoder(r frameReader) *decoder { - return &decoder{ - reader: r, - } -} - -func (d *decoder) Close() { - if d != nil && d.currentCloser != nil { - d.currentCloser.Close() - d.current = nil - d.currentCloser = nil - } -} - -func (d *decoder) Decode(v *packet) error { - ty, r, err := d.reader.NextReader() - if err != nil { - return err - } - if d.current != nil { - d.Close() - } - defer func() { - if d.current == nil { - r.Close() - } - }() - - if ty != engineio.MessageText { - return fmt.Errorf("need text package") - } - reader := bufio.NewReader(r) - - v.Id = -1 - - t, err := reader.ReadByte() - if err != nil { - return err - } - v.Type = packetType(t - '0') - - if v.Type == _BINARY_EVENT || v.Type == _BINARY_ACK { - num, err := reader.ReadBytes('-') - if err != nil { - return err - } - numLen := len(num) - if numLen == 0 { - return fmt.Errorf("invalid packet") - } - n, err := strconv.ParseInt(string(num[:numLen-1]), 10, 64) - if err != nil { - return fmt.Errorf("invalid packet") - } - v.attachNumber = int(n) - } - - next, err := reader.Peek(1) - if err == io.EOF { - return nil - } - if err != nil { - return err - } - if len(next) == 0 { - return fmt.Errorf("invalid packet") - } - - if next[0] == '/' { - path, err := reader.ReadBytes(',') - if err != nil && err != io.EOF { - return err - } - pathLen := len(path) - if pathLen == 0 { - return fmt.Errorf("invalid packet") - } - if err == nil { - path = path[:pathLen-1] - } - v.NSP = string(path) - if err == io.EOF { - return nil - } - } - - id := bytes.NewBuffer(nil) - finish := false - for { - next, err := reader.Peek(1) - if err == io.EOF { - finish = true - break - } - if err != nil { - return err - } - if '0' <= next[0] && next[0] <= '9' { - if err := id.WriteByte(next[0]); err != nil { - return err - } - } else { - break - } - reader.ReadByte() - } - if id.Len() > 0 { - id, err := strconv.ParseInt(id.String(), 10, 64) - if err != nil { - return err - } - v.Id = int(id) - } - if finish { - return nil - } - - switch v.Type { - case _EVENT: - fallthrough - case _BINARY_EVENT: - msgReader, err := newMessageReader(reader) - if err != nil { - return err - } - d.message = msgReader.Message() - d.current = msgReader - d.currentCloser = r - case _ACK: - fallthrough - case _BINARY_ACK: - d.current = reader - d.currentCloser = r - } - return nil -} - -func (d *decoder) Message() string { - return d.message -} - -func (d *decoder) DecodeData(v *packet) error { - if d.current == nil { - return nil - } - - decoder := json.NewDecoder(d.current) - if err := decoder.Decode(v.Data); err != nil { - return err - } - if v.Type == _BINARY_EVENT || v.Type == _BINARY_ACK { - binary, err := d.decodeBinary(v.attachNumber) - if err != nil { - return err - } - if err := decodeAttachments(v.Data, binary); err != nil { - return err - } - v.Type -= _BINARY_EVENT - _EVENT - } - return nil -} - -func (d *decoder) decodeBinary(num int) ([][]byte, error) { - ret := make([][]byte, num) - for i := 0; i < num; i++ { - d.currentCloser.Close() - t, r, err := d.reader.NextReader() - if err != nil { - return nil, err - } - d.currentCloser = r - if t == engineio.MessageText { - return nil, fmt.Errorf("need binary") - } - b, err := ioutil.ReadAll(r) - if err != nil { - return nil, err - } - ret[i] = b - } - return ret, nil -} diff --git a/vendor/github.com/googollee/go-socket.io/parser/buffer.go b/vendor/github.com/googollee/go-socket.io/parser/buffer.go new file mode 100644 index 00000000000..8fae47ee5e7 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/parser/buffer.go @@ -0,0 +1,76 @@ +package parser + +import ( + "bytes" + "encoding/json" + "strconv" +) + +// Buffer is an binary buffer handler used in emit args. All buffers will be +// sent as binary in the transport layer. +type Buffer struct { + num uint64 + isBinary bool + + Data []byte +} + +type BufferData struct { + Num uint64 + PlaceHolder bool `json:"_placeholder"` + Data []byte +} + +// MarshalJSON marshals to JSON. +func (a Buffer) MarshalJSON() ([]byte, error) { + var buf bytes.Buffer + if err := a.marshalJSONBuf(&buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (a *Buffer) marshalJSONBuf(buf *bytes.Buffer) error { + encode := a.encodeText + if a.isBinary { + encode = a.encodeBinary + } + + return encode(buf) +} + +func (a *Buffer) encodeText(buf *bytes.Buffer) error { + buf.WriteString(`{"type":"Buffer","data":[`) + for i, d := range a.Data { + if i > 0 { + buf.WriteString(",") + } + buf.WriteString(strconv.Itoa(int(d))) + } + buf.WriteString("]}") + + return nil +} + +func (a *Buffer) encodeBinary(buf *bytes.Buffer) error { + buf.WriteString(`{"_placeholder":true,"num":`) + buf.WriteString(strconv.FormatUint(a.num, 10)) + buf.WriteString("}") + + return nil +} + +// UnmarshalJSON unmarshal data from JSON. +func (a *Buffer) UnmarshalJSON(b []byte) error { + var data BufferData + if err := json.Unmarshal(b, &data); err != nil { + return err + } + + a.isBinary = data.PlaceHolder + a.Data = data.Data + a.num = data.Num + + return nil +} diff --git a/vendor/github.com/googollee/go-socket.io/parser/decoder.go b/vendor/github.com/googollee/go-socket.io/parser/decoder.go new file mode 100644 index 00000000000..2124f8870d5 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/parser/decoder.go @@ -0,0 +1,374 @@ +package parser + +import ( + "bufio" + "bytes" + "encoding/json" + "github.com/googollee/go-socket.io/engineio/session" + "io" + "io/ioutil" + "reflect" + "strings" +) + +const ( + bufferTypeName = "Buffer" +) + +type FrameReader interface { + NextReader() (session.FrameType, io.ReadCloser, error) +} + +type byteReader interface { + io.Reader + + ReadByte() (byte, error) + UnreadByte() error +} + +type Decoder struct { + r FrameReader + + lastFrame io.ReadCloser + packetReader byteReader + + bufferCount uint64 + isEvent bool +} + +func NewDecoder(r FrameReader) *Decoder { + return &Decoder{ + r: r, + } +} + +func (d *Decoder) Close() error { + var err error + + if d.lastFrame != nil { + err = d.lastFrame.Close() + d.lastFrame = nil + } + + return err +} + +func (d *Decoder) DiscardLast() (err error) { + if d.lastFrame != nil { + err = d.lastFrame.Close() + d.lastFrame = nil + } + + return err +} + +func (d *Decoder) DecodeHeader(header *Header, event *string) error { + ft, r, err := d.r.NextReader() + if err != nil { + return err + } + + if ft != session.TEXT { + return errInvalidFirstPacketType + } + + d.lastFrame = r + br, ok := r.(byteReader) + if !ok { + br = bufio.NewReader(r) + } + d.packetReader = br + + bufferCount, err := d.readHeader(header) + if err != nil { + return err + } + + d.bufferCount = bufferCount + if header.Type == binaryEvent || header.Type == binaryAck { + header.Type -= 3 + } + + d.isEvent = header.Type == Event + if d.isEvent { + if err := d.readEvent(event); err != nil { + return err + } + } + return nil +} + +func (d *Decoder) DecodeArgs(types []reflect.Type) ([]reflect.Value, error) { + r := d.packetReader.(io.Reader) + if d.isEvent { + r = io.MultiReader(strings.NewReader("["), r) + } + + ret := make([]reflect.Value, len(types)) + values := make([]interface{}, len(types)) + + for i, typ := range types { + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + ret[i] = reflect.New(typ) + values[i] = ret[i].Interface() + } + + if err := json.NewDecoder(r).Decode(&values); err != nil { + if err == io.EOF { + err = nil + } + _ = d.DiscardLast() + return nil, err + } + + //we can't use defer or call DiscardLast before decoding, because + //there are buffered readers involved and if we invoke .Close() json will encounter unexpected EOF. + _ = d.DiscardLast() + + for i, typ := range types { + if typ.Kind() != reflect.Ptr { + ret[i] = ret[i].Elem() + } + } + + buffers := make([]Buffer, d.bufferCount) + for i := range buffers { + ft, r, err := d.r.NextReader() + if err != nil { + return nil, err + } + + buffers[i].Data, err = d.readBuffer(ft, r) + if err != nil { + return nil, err + } + } + + for i := range ret { + if err := d.detachBuffer(ret[i], buffers); err != nil { + return nil, err + } + } + return ret, nil +} + +func (d *Decoder) readUint64FromText(r byteReader) (uint64, bool, error) { + var ret uint64 + var hasRead bool + + for { + b, err := r.ReadByte() + if err != nil { + if hasRead { + return ret, true, nil + } + return 0, false, err + } + + if !('0' <= b && b <= '9') { + _ = r.UnreadByte() + return ret, hasRead, nil + } + hasRead = true + ret = ret*10 + uint64(b-'0') + } +} + +func (d *Decoder) readString(r byteReader, until byte) (string, error) { + var ret bytes.Buffer + var hasRead bool + + for { + b, err := r.ReadByte() + if err != nil { + if hasRead { + return ret.String(), nil + } + return "", err + } + + if b == until { + return ret.String(), nil + } + + if err := ret.WriteByte(b); err != nil { + return "", err + } + hasRead = true + } +} + +func (d *Decoder) readHeader(header *Header) (uint64, error) { + typ, err := d.packetReader.ReadByte() + if err != nil { + return 0, err + } + + header.Type = Type(typ - '0') + if header.Type > binaryAck { + return 0, ErrInvalidPacketType + } + + num, hasNum, err := d.readUint64FromText(d.packetReader) + if err != nil { + if err == io.EOF { + err = nil + } + + return 0, err + } + + nextByte, err := d.packetReader.ReadByte() + if err != nil { + header.ID = num + header.NeedAck = hasNum + + if err == io.EOF { + err = nil + } + + return 0, err + } + + // check if buffer count + var bufferCount uint64 + if nextByte == '-' { + bufferCount = num + hasNum = false + num = 0 + } else { + _ = d.packetReader.UnreadByte() + } + + // check namespace + nextByte, err = d.packetReader.ReadByte() + if err != nil { + if err == io.EOF { + err = nil + } + return bufferCount, err + } + + if nextByte == '/' { + _ = d.packetReader.UnreadByte() + header.Namespace, err = d.readString(d.packetReader, ',') + if err != nil { + if err == io.EOF { + err = nil + } + return bufferCount, err + } + + queryPos := strings.IndexByte(header.Namespace, '?') + if queryPos > -1 { + header.Query = header.Namespace[queryPos+1:] + header.Namespace = header.Namespace[:queryPos] + } + } else { + _ = d.packetReader.UnreadByte() + } + + // read id + header.ID, header.NeedAck, err = d.readUint64FromText(d.packetReader) + if err != nil { + if err == io.EOF { + err = nil + } + + return bufferCount, err + } + + if !header.NeedAck { + // 313["data"], id has been read at beginning, need add back. + header.ID = num + header.NeedAck = hasNum + } + + return bufferCount, err +} + +func (d *Decoder) readEvent(event *string) error { + b, err := d.packetReader.ReadByte() + if err != nil { + return err + } + + if b != '[' { + _ = d.packetReader.UnreadByte() + + return nil + } + + var buf bytes.Buffer + for { + b, err := d.packetReader.ReadByte() + if err != nil { + return err + } + + if b == ',' { + break + } + if b == ']' { + _ = d.packetReader.UnreadByte() + break + } + + buf.WriteByte(b) + } + + return json.Unmarshal(buf.Bytes(), event) +} + +func (d *Decoder) readBuffer(ft session.FrameType, r io.ReadCloser) ([]byte, error) { + defer r.Close() + + if ft != session.BINARY { + return nil, errInvalidBinaryBufferType + } + + return ioutil.ReadAll(r) +} + +func (d *Decoder) detachBuffer(v reflect.Value, buffers []Buffer) error { + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + v = v.Elem() + } + + switch v.Kind() { + case reflect.Struct: + if v.Type().Name() == bufferTypeName { + if !v.CanAddr() { + return errFailedBufferAddress + } + buffer := v.Addr().Interface().(*Buffer) + if buffer.isBinary { + *buffer = buffers[buffer.num] + } + return nil + } + for i := 0; i < v.NumField(); i++ { + if err := d.detachBuffer(v.Field(i), buffers); err != nil { + return err + } + } + + case reflect.Map: + for _, key := range v.MapKeys() { + if err := d.detachBuffer(v.MapIndex(key), buffers); err != nil { + return err + } + } + + case reflect.Array, reflect.Slice: + for i := 0; i < v.Len(); i++ { + if err := d.detachBuffer(v.Index(i), buffers); err != nil { + return err + } + } + } + + return nil +} diff --git a/vendor/github.com/googollee/go-socket.io/parser/encoder.go b/vendor/github.com/googollee/go-socket.io/parser/encoder.go new file mode 100644 index 00000000000..9b46feebcb5 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/parser/encoder.go @@ -0,0 +1,196 @@ +package parser + +import ( + "bufio" + "encoding/json" + "github.com/googollee/go-socket.io/engineio/session" + "io" + "reflect" +) + +type FrameWriter interface { + NextWriter(ft session.FrameType) (io.WriteCloser, error) +} + +type Encoder struct { + w FrameWriter +} + +func NewEncoder(w FrameWriter) *Encoder { + return &Encoder{ + w: w, + } +} + +func (e *Encoder) Encode(h Header, args ...interface{}) (err error) { + var w io.WriteCloser + w, err = e.w.NextWriter(session.TEXT) + if err != nil { + return + } + + var buffers [][]byte + buffers, err = e.writePacket(w, h, args) + if err != nil { + return + } + + for _, b := range buffers { + w, err = e.w.NextWriter(session.BINARY) + if err != nil { + return + } + + err = e.writeBuffer(w, b) + if err != nil { + return + } + } + return +} + +type byteWriter interface { + io.Writer + WriteByte(byte) error +} + +type flusher interface { + Flush() error +} + +func (e *Encoder) writePacket(w io.WriteCloser, h Header, args []interface{}) ([][]byte, error) { + defer w.Close() + + bw, ok := w.(byteWriter) + if !ok { + bw = bufio.NewWriter(w) + } + + max := uint64(0) + buffers, err := e.attachBuffer(reflect.ValueOf(args), &max) + if err != nil { + return nil, err + } + + if len(buffers) > 0 && (h.Type == Event || h.Type == Ack) { + h.Type += 3 + } + + if err := bw.WriteByte(byte(h.Type + '0')); err != nil { + return nil, err + } + + if h.Type == binaryAck || h.Type == binaryEvent { + if err := e.writeUint64(bw, max); err != nil { + return nil, err + } + if err := bw.WriteByte('-'); err != nil { + return nil, err + } + } + + if h.Namespace != "" { + if _, err := bw.Write([]byte(h.Namespace)); err != nil { + return nil, err + } + if h.ID != 0 || args != nil { + if err := bw.WriteByte(','); err != nil { + return nil, err + } + } + } + + if h.NeedAck { + if err := e.writeUint64(bw, h.ID); err != nil { + return nil, err + } + } + + if len(args) > 0 { + if err := json.NewEncoder(bw).Encode(args[0]); err != nil { + return nil, err + } + } + + if f, ok := bw.(flusher); ok { + if err := f.Flush(); err != nil { + return nil, err + } + } + + return buffers, nil +} + +func (e *Encoder) writeUint64(w byteWriter, i uint64) error { + base := uint64(1) + for i/base >= 10 { + base *= 10 + } + for base > 0 { + p := i / base + if err := w.WriteByte(byte(p) + '0'); err != nil { + return err + } + i -= p * base + base /= 10 + } + return nil +} + +func (e *Encoder) attachBuffer(v reflect.Value, index *uint64) ([][]byte, error) { + for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { + v = v.Elem() + } + + var ret [][]byte + switch v.Kind() { + case reflect.Struct: + if v.Type().Name() == bufferTypeName { + if !v.CanAddr() { + return nil, errFailedBufferAddress + } + buffer := v.Addr().Interface().(*Buffer) + buffer.num = *index + buffer.isBinary = true + ret = append(ret, buffer.Data) + *index++ + } else { + for i := 0; i < v.NumField(); i++ { + b, err := e.attachBuffer(v.Field(i), index) + if err != nil { + return nil, err + } + ret = append(ret, b...) + } + } + + case reflect.Array, reflect.Slice: + for i := 0; i < v.Len(); i++ { + b, err := e.attachBuffer(v.Index(i), index) + if err != nil { + return nil, err + } + + ret = append(ret, b...) + } + + case reflect.Map: + for _, key := range v.MapKeys() { + b, err := e.attachBuffer(v.MapIndex(key), index) + if err != nil { + return nil, err + } + + ret = append(ret, b...) + } + } + + return ret, nil +} + +func (e *Encoder) writeBuffer(w io.WriteCloser, buffer []byte) error { + defer w.Close() + + _, err := w.Write(buffer) + return err +} diff --git a/vendor/github.com/googollee/go-socket.io/parser/errors.go b/vendor/github.com/googollee/go-socket.io/parser/errors.go new file mode 100644 index 00000000000..726f045971a --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/parser/errors.go @@ -0,0 +1,13 @@ +package parser + +import "errors" + +var ( + ErrInvalidPacketType = errors.New("invalid packet type") + + errInvalidBinaryBufferType = errors.New("buffer packet should be binary") + + errInvalidFirstPacketType = errors.New("first packet should be text frame") + + errFailedBufferAddress = errors.New("can't get Buffer address") +) diff --git a/vendor/github.com/googollee/go-socket.io/parser/packet.go b/vendor/github.com/googollee/go-socket.io/parser/packet.go new file mode 100644 index 00000000000..f32538546f1 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/parser/packet.go @@ -0,0 +1,38 @@ +package parser + +// Type of packet. +type Type byte + +const ( + // Connect type + Connect Type = iota + // Disconnect type + Disconnect + // Event type + Event + // Ack type + Ack + // Error type + Error + + // BinaryEvent type + binaryEvent + // BinaryAck type + binaryAck +) + +// Header of packet. +type Header struct { + Type Type + ID uint64 + NeedAck bool + Namespace string + Query string +} + +// Payload of packet. +type Payload struct { + Header Header + + Data []interface{} +} diff --git a/vendor/github.com/googollee/go-socket.io/redis_broadcast.go b/vendor/github.com/googollee/go-socket.io/redis_broadcast.go new file mode 100644 index 00000000000..b601aaa81a6 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/redis_broadcast.go @@ -0,0 +1,565 @@ +package socketio + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "sync" + + "github.com/gomodule/redigo/redis" +) + +// redisBroadcast gives Join, Leave & BroadcastTO server API support to socket.io along with room management +// map of rooms where each room contains a map of connection id to connections in that room +type redisBroadcast struct { + pub *redis.PubSubConn + sub *redis.PubSubConn + + nsp string + uid string + key string + reqChannel string + resChannel string + + requests map[string]interface{} + + rooms map[string]map[string]Conn + + lock sync.RWMutex +} + +// request types +const ( + roomLenReqType = "0" + clearRoomReqType = "1" + allRoomReqType = "2" +) + +// request structs +type roomLenRequest struct { + RequestType string + RequestID string + Room string + numSub int `json:"-"` + msgCount int `json:"-"` + connections int `json:"-"` + mutex sync.Mutex `json:"-"` + done chan bool `json:"-"` +} + +type clearRoomRequest struct { + RequestType string + RequestID string + Room string + UUID string +} + +type allRoomRequest struct { + RequestType string + RequestID string + rooms map[string]bool `json:"-"` + numSub int `json:"-"` + msgCount int `json:"-"` + mutex sync.Mutex `json:"-"` + done chan bool `json:"-"` +} + +// response struct +type roomLenResponse struct { + RequestType string + RequestID string + Connections int +} + +type allRoomResponse struct { + RequestType string + RequestID string + Rooms []string +} + +func newRedisBroadcast(nsp string, opts *RedisAdapterOptions) (*redisBroadcast, error) { + addr := opts.getAddr() + var redisOpts []redis.DialOption + if len(opts.Password) > 0 { + redisOpts = append(redisOpts, redis.DialPassword(opts.Password)) + } + if opts.DB > 0 { + redisOpts = append(redisOpts, redis.DialDatabase(opts.DB)) + } + + pub, err := redis.Dial(opts.Network, addr, redisOpts...) + if err != nil { + return nil, err + } + + sub, err := redis.Dial(opts.Network, addr, redisOpts...) + if err != nil { + return nil, err + } + + subConn := &redis.PubSubConn{Conn: sub} + pubConn := &redis.PubSubConn{Conn: pub} + + if err = subConn.PSubscribe(fmt.Sprintf("%s#%s#*", opts.Prefix, nsp)); err != nil { + return nil, err + } + + uid := newV4UUID() + rbc := &redisBroadcast{ + rooms: make(map[string]map[string]Conn), + requests: make(map[string]interface{}), + sub: subConn, + pub: pubConn, + key: fmt.Sprintf("%s#%s#%s", opts.Prefix, nsp, uid), + reqChannel: fmt.Sprintf("%s-request#%s", opts.Prefix, nsp), + resChannel: fmt.Sprintf("%s-response#%s", opts.Prefix, nsp), + nsp: nsp, + uid: uid, + } + + if err = subConn.Subscribe(rbc.reqChannel, rbc.resChannel); err != nil { + return nil, err + } + + go rbc.dispatch() + + return rbc, nil +} + +// AllRooms gives list of all rooms available for redisBroadcast. +func (bc *redisBroadcast) AllRooms() []string { + req := allRoomRequest{ + RequestType: allRoomReqType, + RequestID: newV4UUID(), + } + reqJSON, _ := json.Marshal(&req) + + req.rooms = make(map[string]bool) + numSub, _ := bc.getNumSub(bc.reqChannel) + req.numSub = numSub + req.done = make(chan bool, 1) + + bc.requests[req.RequestID] = &req + _, err := bc.pub.Conn.Do("PUBLISH", bc.reqChannel, reqJSON) + if err != nil { + return []string{} // if error occurred,return empty + } + + <-req.done + + rooms := make([]string, 0, len(req.rooms)) + for room := range req.rooms { + rooms = append(rooms, room) + } + + delete(bc.requests, req.RequestID) + return rooms +} + +// Join joins the given connection to the redisBroadcast room. +func (bc *redisBroadcast) Join(room string, connection Conn) { + bc.lock.Lock() + defer bc.lock.Unlock() + + if _, ok := bc.rooms[room]; !ok { + bc.rooms[room] = make(map[string]Conn) + } + + bc.rooms[room][connection.ID()] = connection +} + +// Leave leaves the given connection from given room (if exist) +func (bc *redisBroadcast) Leave(room string, connection Conn) { + bc.lock.Lock() + defer bc.lock.Unlock() + + if connections, ok := bc.rooms[room]; ok { + delete(connections, connection.ID()) + + if len(connections) == 0 { + delete(bc.rooms, room) + } + } +} + +// LeaveAll leaves the given connection from all rooms. +func (bc *redisBroadcast) LeaveAll(connection Conn) { + bc.lock.Lock() + defer bc.lock.Unlock() + + for room, connections := range bc.rooms { + delete(connections, connection.ID()) + + if len(connections) == 0 { + delete(bc.rooms, room) + } + } +} + +// Clear clears the room. +func (bc *redisBroadcast) Clear(room string) { + bc.lock.Lock() + defer bc.lock.Unlock() + + delete(bc.rooms, room) + go bc.publishClear(room) +} + +// Send sends given event & args to all the connections in the specified room. +func (bc *redisBroadcast) Send(room, event string, args ...interface{}) { + bc.lock.RLock() + defer bc.lock.RUnlock() + + connections, ok := bc.rooms[room] + if ok { + for _, connection := range connections { + connection.Emit(event, args...) + } + } + + bc.publishMessage(room, event, args...) +} + +// SendAll sends given event & args to all the connections to all the rooms. +func (bc *redisBroadcast) SendAll(event string, args ...interface{}) { + bc.lock.RLock() + defer bc.lock.RUnlock() + + for _, connections := range bc.rooms { + for _, connection := range connections { + connection.Emit(event, args...) + } + } + bc.publishMessage("", event, args...) +} + +// ForEach sends data returned by DataFunc, if room does not exits sends nothing. +func (bc *redisBroadcast) ForEach(room string, f EachFunc) { + bc.lock.RLock() + defer bc.lock.RUnlock() + + occupants, ok := bc.rooms[room] + if !ok { + return + } + + for _, connection := range occupants { + f(connection) + } +} + +// Len gives number of connections in the room. +func (bc *redisBroadcast) Len(room string) int { + req := roomLenRequest{ + RequestType: roomLenReqType, + RequestID: newV4UUID(), + Room: room, + } + + reqJSON, err := json.Marshal(&req) + if err != nil { + return -1 + } + + numSub, err := bc.getNumSub(bc.reqChannel) + if err != nil { + return -1 + } + + req.numSub = numSub + + req.done = make(chan bool, 1) + + bc.requests[req.RequestID] = &req + _, err = bc.pub.Conn.Do("PUBLISH", bc.reqChannel, reqJSON) + if err != nil { + return -1 + } + + <-req.done + + delete(bc.requests, req.RequestID) + return req.connections +} + +// Rooms gives the list of all the rooms available for redisBroadcast in case of +// no connection is given, in case of a connection is given, it gives +// list of all the rooms the connection is joined to. +func (bc *redisBroadcast) Rooms(connection Conn) []string { + bc.lock.RLock() + defer bc.lock.RUnlock() + + if connection == nil { + return bc.AllRooms() + } + + return bc.getRoomsByConn(connection) +} + +func (bc *redisBroadcast) onMessage(channel string, msg []byte) error { + channelParts := strings.Split(channel, "#") + nsp := channelParts[len(channelParts)-2] + if bc.nsp != nsp { + return nil + } + + uid := channelParts[len(channelParts)-1] + if bc.uid == uid { + return nil + } + + var bcMessage map[string][]interface{} + err := json.Unmarshal(msg, &bcMessage) + if err != nil { + return errors.New("invalid broadcast message") + } + + args := bcMessage["args"] + opts := bcMessage["opts"] + + room, ok := opts[0].(string) + if !ok { + return errors.New("invalid room") + } + + event, ok := opts[1].(string) + if !ok { + return errors.New("invalid event") + } + + if room != "" { + bc.send(room, event, args...) + } else { + bc.sendAll(event, args...) + } + + return nil +} + +// Get the number of subscribers of a channel. +func (bc *redisBroadcast) getNumSub(channel string) (int, error) { + rs, err := bc.pub.Conn.Do("PUBSUB", "NUMSUB", channel) + if err != nil { + return 0, err + } + + numSub64, ok := rs.([]interface{})[1].(int64) + if !ok { + return 0, errors.New("redis reply cast to int error") + } + return int(numSub64), nil +} + +// Handle request from redis channel. +func (bc *redisBroadcast) onRequest(msg []byte) { + var req map[string]string + + if err := json.Unmarshal(msg, &req); err != nil { + return + } + + var res interface{} + switch req["RequestType"] { + case roomLenReqType: + res = roomLenResponse{ + RequestType: req["RequestType"], + RequestID: req["RequestID"], + Connections: len(bc.rooms[req["Room"]]), + } + bc.publish(bc.resChannel, &res) + + case allRoomReqType: + res := allRoomResponse{ + RequestType: req["RequestType"], + RequestID: req["RequestID"], + Rooms: bc.allRooms(), + } + bc.publish(bc.resChannel, &res) + + case clearRoomReqType: + if bc.uid == req["UUID"] { + return + } + bc.clear(req["Room"]) + + default: + } +} + +func (bc *redisBroadcast) publish(channel string, msg interface{}) { + resJSON, err := json.Marshal(msg) + if err != nil { + return + } + + _, err = bc.pub.Conn.Do("PUBLISH", channel, resJSON) + if err != nil { + return + } +} + +// Handle response from redis channel. +func (bc *redisBroadcast) onResponse(msg []byte) { + var res map[string]interface{} + + err := json.Unmarshal(msg, &res) + if err != nil { + return + } + + req, ok := bc.requests[res["RequestID"].(string)] + if !ok { + return + } + + switch res["RequestType"] { + case roomLenReqType: + roomLenReq := req.(*roomLenRequest) + + roomLenReq.mutex.Lock() + roomLenReq.msgCount++ + roomLenReq.connections += int(res["Connections"].(float64)) + roomLenReq.mutex.Unlock() + + if roomLenReq.numSub == roomLenReq.msgCount { + roomLenReq.done <- true + } + + case allRoomReqType: + allRoomReq := req.(*allRoomRequest) + rooms, ok := res["Rooms"].([]interface{}) + if !ok { + allRoomReq.done <- true + return + } + + allRoomReq.mutex.Lock() + allRoomReq.msgCount++ + for _, room := range rooms { + allRoomReq.rooms[room.(string)] = true + } + allRoomReq.mutex.Unlock() + + if allRoomReq.numSub == allRoomReq.msgCount { + allRoomReq.done <- true + } + + default: + } +} + +func (bc *redisBroadcast) publishClear(room string) { + req := clearRoomRequest{ + RequestType: clearRoomReqType, + RequestID: newV4UUID(), + Room: room, + UUID: bc.uid, + } + + bc.publish(bc.reqChannel, &req) +} + +func (bc *redisBroadcast) clear(room string) { + bc.lock.Lock() + defer bc.lock.Unlock() + + delete(bc.rooms, room) +} + +func (bc *redisBroadcast) send(room string, event string, args ...interface{}) { + bc.lock.RLock() + defer bc.lock.RUnlock() + + connections, ok := bc.rooms[room] + if !ok { + return + } + + for _, connection := range connections { + connection.Emit(event, args...) + } +} + +func (bc *redisBroadcast) publishMessage(room string, event string, args ...interface{}) { + opts := make([]interface{}, 2) + opts[0] = room + opts[1] = event + + bcMessage := map[string][]interface{}{ + "opts": opts, + "args": args, + } + bcMessageJSON, err := json.Marshal(bcMessage) + if err != nil { + return + } + + _, err = bc.pub.Conn.Do("PUBLISH", bc.key, bcMessageJSON) + if err != nil { + return + } +} + +func (bc *redisBroadcast) sendAll(event string, args ...interface{}) { + bc.lock.RLock() + defer bc.lock.RUnlock() + + for _, connections := range bc.rooms { + for _, connection := range connections { + connection.Emit(event, args...) + } + } +} + +func (bc *redisBroadcast) allRooms() []string { + bc.lock.RLock() + defer bc.lock.RUnlock() + + rooms := make([]string, 0, len(bc.rooms)) + for room := range bc.rooms { + rooms = append(rooms, room) + } + + return rooms +} + +func (bc *redisBroadcast) getRoomsByConn(connection Conn) []string { + var rooms []string + + for room, connections := range bc.rooms { + if _, ok := connections[connection.ID()]; ok { + rooms = append(rooms, room) + } + } + + return rooms +} + +func (bc *redisBroadcast) dispatch() { + for { + switch m := bc.sub.Receive().(type) { + case redis.Message: + if m.Channel == bc.reqChannel { + bc.onRequest(m.Data) + break + } else if m.Channel == bc.resChannel { + bc.onResponse(m.Data) + break + } + + err := bc.onMessage(m.Channel, m.Data) + if err != nil { + return + } + + case redis.Subscription: + if m.Count == 0 { + return + } + + case error: + return + } + } +} diff --git a/vendor/github.com/googollee/go-socket.io/server.go b/vendor/github.com/googollee/go-socket.io/server.go index 02fd438637d..90fb8aa8af4 100644 --- a/vendor/github.com/googollee/go-socket.io/server.go +++ b/vendor/github.com/googollee/go-socket.io/server.go @@ -1,111 +1,341 @@ package socketio import ( - "github.com/googollee/go-engine.io" + "errors" "net/http" - "time" + + "github.com/gomodule/redigo/redis" + + "github.com/googollee/go-socket.io/engineio" + "github.com/googollee/go-socket.io/parser" ) -// Server is the server of socket.io. +// Server is a go-socket.io server. type Server struct { - *namespace - broadcast BroadcastAdaptor - eio *engineio.Server + engine *engineio.Server + + handlers *namespaceHandlers + + redisAdapter *RedisAdapterOptions +} + +// NewServer returns a server. +func NewServer(opts *engineio.Options) *Server { + return &Server{ + handlers: newNamespaceHandlers(), + engine: engineio.NewServer(opts), + } } -// NewServer returns the server supported given transports. If transports is nil, the server will use ["polling", "websocket"] as default. -func NewServer(transportNames []string) (*Server, error) { - eio, err := engineio.NewServer(transportNames) +// Adapter sets redis broadcast adapter. +func (s *Server) Adapter(opts *RedisAdapterOptions) (bool, error) { + opts = getOptions(opts) + var redisOpts []redis.DialOption + if len(opts.Password) > 0 { + redisOpts = append(redisOpts, redis.DialPassword(opts.Password)) + } + if opts.DB > 0 { + redisOpts = append(redisOpts, redis.DialDatabase(opts.DB)) + } + + conn, err := redis.Dial(opts.Network, opts.getAddr(), redisOpts...) if err != nil { - return nil, err + return false, err } - ret := &Server{ - namespace: newNamespace(newBroadcastDefault()), - eio: eio, + + s.redisAdapter = opts + + conn.Close() + return true, nil +} + +// Close closes server. +func (s *Server) Close() error { + return s.engine.Close() +} + +// ServeHTTP dispatches the request to the handler whose pattern most closely matches the request URL. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.engine.ServeHTTP(w, r) +} + +// OnConnect set a handler function f to handle open event for namespace. +func (s *Server) OnConnect(namespace string, f func(Conn) error) { + h := s.getNamespace(namespace) + if h == nil { + h = s.createNamespace(namespace) } - go ret.loop() - return ret, nil + + h.OnConnect(f) } -// SetPingTimeout sets the timeout of a connection ping. When it times out, the server will close the connection with the client. Default is 60s. -func (s *Server) SetPingTimeout(t time.Duration) { - s.eio.SetPingTimeout(t) +// OnDisconnect set a handler function f to handle disconnect event for namespace. +func (s *Server) OnDisconnect(namespace string, f func(Conn, string)) { + h := s.getNamespace(namespace) + if h == nil { + h = s.createNamespace(namespace) + } + + h.OnDisconnect(f) } -// SetPingInterval sets the interval of pings. Default is 25s. -func (s *Server) SetPingInterval(t time.Duration) { - s.eio.SetPingInterval(t) +// OnError set a handler function f to handle error for namespace. +func (s *Server) OnError(namespace string, f func(Conn, error)) { + h := s.getNamespace(namespace) + if h == nil { + h = s.createNamespace(namespace) + } + + h.OnError(f) } -// SetMaxConnection sets the maximum number of connections with clients. Default is 1000. -func (s *Server) SetMaxConnection(n int) { - s.eio.SetMaxConnection(n) +// OnEvent set a handler function f to handle event for namespace. +func (s *Server) OnEvent(namespace, event string, f interface{}) { + h := s.getNamespace(namespace) + if h == nil { + h = s.createNamespace(namespace) + } + + h.OnEvent(event, f) } -// GetMaxConnection returns the current max connection -func (s *Server) GetMaxConnection() int { - return s.eio.GetMaxConnection() +// Serve serves go-socket.io server. +func (s *Server) Serve() error { + for { + conn, err := s.engine.Accept() + //todo maybe need check EOF from Accept() + if err != nil { + return err + } + + go s.serveConn(conn) + } } -// Count returns the current number of connected clients in session -func (s *Server) Count() int { - return s.eio.Count() +// JoinRoom joins given connection to the room. +func (s *Server) JoinRoom(namespace string, room string, connection Conn) bool { + nspHandler := s.getNamespace(namespace) + if nspHandler != nil { + nspHandler.broadcast.Join(room, connection) + return true + } + + return false } -// LenRoom returns the current number of connected clients in room -func (s *Server) LenRoom(room string) int { - return s.namespace.broadcast.Len(room) +// LeaveRoom leaves given connection from the room. +func (s *Server) LeaveRoom(namespace string, room string, connection Conn) bool { + nspHandler := s.getNamespace(namespace) + if nspHandler != nil { + nspHandler.broadcast.Leave(room, connection) + return true + } + + return false } -// SetAllowRequest sets the middleware function when a connection is established. If a non-nil value is returned, the connection won't be established. Default will allow all connections. -func (s *Server) SetAllowRequest(f func(*http.Request) error) { - s.eio.SetAllowRequest(f) +// LeaveAllRooms leaves the given connection from all rooms. +func (s *Server) LeaveAllRooms(namespace string, connection Conn) bool { + nspHandler := s.getNamespace(namespace) + if nspHandler != nil { + nspHandler.broadcast.LeaveAll(connection) + return true + } + + return false } -// SetAllowUpgrades sets whether server allows transport upgrades. Default is true. -func (s *Server) SetAllowUpgrades(allow bool) { - s.eio.SetAllowUpgrades(allow) +// ClearRoom clears the room. +func (s *Server) ClearRoom(namespace string, room string) bool { + nspHandler := s.getNamespace(namespace) + if nspHandler != nil { + nspHandler.broadcast.Clear(room) + return true + } + + return false } -// SetCookie sets the name of the cookie used by engine.io. Default is "io". -func (s *Server) SetCookie(prefix string) { - s.eio.SetCookie(prefix) +// BroadcastToRoom broadcasts given event & args to all the connections in the room. +func (s *Server) BroadcastToRoom(namespace string, room, event string, args ...interface{}) bool { + nspHandler := s.getNamespace(namespace) + if nspHandler != nil { + nspHandler.broadcast.Send(room, event, args...) + return true + } + + return false } -// SetNewId sets the callback func to generate new connection id. By default, id is generated from remote address + current time stamp -func (s *Server) SetNewId(f func(*http.Request) string) { - s.eio.SetNewId(f) +// BroadcastToNamespace broadcasts given event & args to all the connections in the same namespace. +func (s *Server) BroadcastToNamespace(namespace string, event string, args ...interface{}) bool { + nspHandler := s.getNamespace(namespace) + if nspHandler != nil { + nspHandler.broadcast.SendAll(event, args...) + return true + } + + return false } -// SetSessionsManager sets the sessions as server's session manager. Default sessions is a single process manager. You can customize it as a load balancer. -func (s *Server) SetSessionManager(sessions engineio.Sessions) { - s.eio.SetSessionManager(sessions) +// RoomLen gives number of connections in the room. +func (s *Server) RoomLen(namespace string, room string) int { + nspHandler := s.getNamespace(namespace) + if nspHandler != nil { + return nspHandler.broadcast.Len(room) + } + + return -1 } -// SetAdaptor sets the adaptor of broadcast. Default is an in-process broadcast implementation. -func (s *Server) SetAdaptor(adaptor BroadcastAdaptor) { - s.namespace = newNamespace(adaptor) +// Rooms gives list of all the rooms. +func (s *Server) Rooms(namespace string) []string { + nspHandler := s.getNamespace(namespace) + if nspHandler != nil { + return nspHandler.broadcast.Rooms(nil) + } + + return nil } -// ServeHTTP handles http requests. -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - s.eio.ServeHTTP(w, r) +// Count number of connections. +func (s *Server) Count() int { + return s.engine.Count() +} + +// ForEach sends data by DataFunc, if room does not exit sends anything. +func (s *Server) ForEach(namespace string, room string, f EachFunc) bool { + nspHandler := s.getNamespace(namespace) + if nspHandler != nil { + nspHandler.broadcast.ForEach(room, f) + return true + } + + return false +} + +func (s *Server) serveConn(conn engineio.Conn) { + c := newConn(conn, s.handlers) + if err := c.connect(); err != nil { + _ = c.Close() + if root, ok := s.handlers.Get(rootNamespace); ok && root.onError != nil { + root.onError(nil, err) + } + + return + } + + go s.serveError(c) + go s.serveWrite(c) + go s.serveRead(c) } -// BroadcastTo is a server level broadcast function. -func (s *Server) BroadcastTo(room, event string, args ...interface{}) { - s.namespace.BroadcastTo(room, event, args...) +func (s *Server) serveError(c *conn) { + defer func() { + c.Close() + s.engine.Remove(c.ID()) + }() + + for { + select { + case <-c.quitChan: + return + case err := <-c.errorChan: + var errMsg *errorMessage + if !errors.As(err, &errMsg) { + continue + } + + if handler := c.namespace(errMsg.namespace); handler != nil { + if handler.onError != nil { + nsConn, ok := c.namespaces.Get(errMsg.namespace) + if !ok { + continue + } + handler.onError(nsConn, errMsg.err) + } + } + } + } } -func (s *Server) loop() { +func (s *Server) serveWrite(c *conn) { + defer func() { + c.Close() + s.engine.Remove(c.ID()) + }() + for { - conn, err := s.eio.Accept() + select { + case <-c.quitChan: + return + case pkg := <-c.writeChan: + if err := c.encoder.Encode(pkg.Header, pkg.Data); err != nil { + c.onError(pkg.Header.Namespace, err) + } + } + } +} + +func (s *Server) serveRead(c *conn) { + defer func() { + c.Close() + s.engine.Remove(c.ID()) + }() + + var event string + + for { + var header parser.Header + + if err := c.decoder.DecodeHeader(&header, &event); err != nil { + c.onError(rootNamespace, err) + return + } + + if header.Namespace == aliasRootNamespace { + header.Namespace = rootNamespace + } + + var err error + switch header.Type { + case parser.Ack, parser.Connect, parser.Disconnect: + handler, ok := readHandlerMapping[header.Type] + if !ok { + return + } + + err = handler(c, header) + case parser.Event: + err = eventPacketHandler(c, event, header) + } + if err != nil { return } - s := newSocket(conn, s.baseHandler) - go func(s *socket) { - s.loop() - }(s) } } + +func (s *Server) createNamespace(nsp string) *namespaceHandler { + if nsp == aliasRootNamespace { + nsp = rootNamespace + } + + handler := newNamespaceHandler(nsp, s.redisAdapter) + s.handlers.Set(nsp, handler) + + return handler +} + +func (s *Server) getNamespace(nsp string) *namespaceHandler { + if nsp == aliasRootNamespace { + nsp = rootNamespace + } + + ret, ok := s.handlers.Get(nsp) + if !ok { + return nil + } + + return ret +} diff --git a/vendor/github.com/googollee/go-socket.io/socket.go b/vendor/github.com/googollee/go-socket.io/socket.go deleted file mode 100644 index 2c6fa48286f..00000000000 --- a/vendor/github.com/googollee/go-socket.io/socket.go +++ /dev/null @@ -1,174 +0,0 @@ -package socketio - -import ( - "net/http" - "sync" - - "github.com/googollee/go-engine.io" -) - -// Socket is the socket object of socket.io. -type Socket interface { - - // Id returns the session id of socket. - Id() string - - // Rooms returns the rooms name joined now. - Rooms() []string - - // Request returns the first http request when established connection. - Request() *http.Request - - // On registers the function f to handle an event. - On(event string, f interface{}) error - - // Emit emits an event with given args. - Emit(event string, args ...interface{}) error - - // Join joins the room. - Join(room string) error - - // Leave leaves the room. - Leave(room string) error - - // Disconnect disconnect the socket. - Disconnect() - - // BroadcastTo broadcasts an event to the room with given args. - BroadcastTo(room, event string, args ...interface{}) error -} - -type socket struct { - *socketHandler - conn engineio.Conn - namespace string - id int - mu sync.Mutex -} - -func newSocket(conn engineio.Conn, base *baseHandler) *socket { - ret := &socket{ - conn: conn, - } - ret.socketHandler = newSocketHandler(ret, base) - return ret -} - -func (s *socket) Id() string { - return s.conn.Id() -} - -func (s *socket) Request() *http.Request { - return s.conn.Request() -} - -func (s *socket) Emit(event string, args ...interface{}) error { - if err := s.socketHandler.Emit(event, args...); err != nil { - return err - } - if event == "disconnect" { - s.conn.Close() - } - return nil -} - -func (s *socket) Disconnect() { - s.conn.Close() -} - -func (s *socket) send(args []interface{}) error { - packet := packet{ - Type: _EVENT, - Id: -1, - NSP: s.namespace, - Data: args, - } - encoder := newEncoder(s.conn) - return encoder.Encode(packet) -} - -func (s *socket) sendConnect() error { - packet := packet{ - Type: _CONNECT, - Id: -1, - NSP: s.namespace, - } - encoder := newEncoder(s.conn) - return encoder.Encode(packet) -} - -func (s *socket) sendId(args []interface{}) (int, error) { - s.mu.Lock() - packet := packet{ - Type: _EVENT, - Id: s.id, - NSP: s.namespace, - Data: args, - } - s.id++ - if s.id < 0 { - s.id = 0 - } - s.mu.Unlock() - - encoder := newEncoder(s.conn) - err := encoder.Encode(packet) - if err != nil { - return -1, nil - } - return packet.Id, nil -} - -func (s *socket) loop() error { - defer func() { - s.LeaveAll() - p := packet{ - Type: _DISCONNECT, - Id: -1, - } - s.socketHandler.onPacket(nil, &p) - }() - - p := packet{ - Type: _CONNECT, - Id: -1, - } - encoder := newEncoder(s.conn) - if err := encoder.Encode(p); err != nil { - return err - } - s.socketHandler.onPacket(nil, &p) - for { - decoder := newDecoder(s.conn) - var p packet - if err := decoder.Decode(&p); err != nil { - return err - } - ret, err := s.socketHandler.onPacket(decoder, &p) - if err != nil { - return err - } - switch p.Type { - case _CONNECT: - s.namespace = p.NSP - s.sendConnect() - case _BINARY_EVENT: - fallthrough - case _EVENT: - if p.Id >= 0 { - p := packet{ - Type: _ACK, - Id: p.Id, - NSP: s.namespace, - Data: ret, - } - encoder := newEncoder(s.conn) - if err := encoder.Encode(p); err != nil { - return err - } - } - case _DISCONNECT: - return nil - } - } -} diff --git a/vendor/github.com/googollee/go-socket.io/trim_writer.go b/vendor/github.com/googollee/go-socket.io/trim_writer.go deleted file mode 100644 index b6f3880eefd..00000000000 --- a/vendor/github.com/googollee/go-socket.io/trim_writer.go +++ /dev/null @@ -1,45 +0,0 @@ -package socketio - -import ( - "bytes" - "io" -) - -type trimWriter struct { - trimChars string - trimBuf []byte - output io.Writer -} - -func newTrimWriter(w io.Writer, trimChars string) *trimWriter { - return &trimWriter{ - trimChars: trimChars, - output: w, - } -} - -func (w *trimWriter) Write(p []byte) (int, error) { - out := bytes.TrimRight(p, w.trimChars) - buf := p[len(out):] - var written int - if (len(out) > 0) && (w.trimBuf != nil) { - var err error - if written, err = w.output.Write(w.trimBuf); err != nil { - return 0, err - } - w.trimBuf = nil - } - if w.trimBuf != nil { - w.trimBuf = append(w.trimBuf, buf...) - } else { - w.trimBuf = buf - } - if len(p) == 0 { - return written, nil - } - ret, err := w.output.Write(out) - if err != nil { - return 0, err - } - return written + ret, nil -} diff --git a/vendor/github.com/googollee/go-socket.io/types.go b/vendor/github.com/googollee/go-socket.io/types.go new file mode 100644 index 00000000000..cb9513bdf0f --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/types.go @@ -0,0 +1,23 @@ +package socketio + +import ( + "github.com/googollee/go-socket.io/parser" + "reflect" +) + +// namespace +const ( + aliasRootNamespace = "/" + rootNamespace = "" +) + +// message +const ( + clientDisconnectMsg = "client namespace disconnect" +) + +type readHandler func(c *conn, header parser.Header) error + +var ( + defaultHeaderType = []reflect.Type{reflect.TypeOf("")} +) \ No newline at end of file diff --git a/vendor/github.com/googollee/go-socket.io/upgrade workflow.md b/vendor/github.com/googollee/go-socket.io/upgrade workflow.md new file mode 100644 index 00000000000..ff4881c8bdc --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/upgrade workflow.md @@ -0,0 +1,14 @@ +```mermaid +sequenceDiagram +client->>server: dial +server->>client: reply open +client->>server: dial upgrade +client->>server: upgrade ping probe +server->>client: upgrade pong probe +client->>client: pause old conn +client->>client: switch old conn to upgraded conn +client->>server: upgrade +server->>server: pause old conn(return noop if waiting) +server->>server: switch old conn to upgraded conn +server->>server: close old conn +``` diff --git a/vendor/github.com/gorilla/websocket/README.md b/vendor/github.com/gorilla/websocket/README.md index 0827d059c11..19aa2e75c82 100644 --- a/vendor/github.com/gorilla/websocket/README.md +++ b/vendor/github.com/gorilla/websocket/README.md @@ -8,7 +8,7 @@ Gorilla WebSocket is a [Go](http://golang.org/) implementation of the ### Documentation -* [API Reference](http://godoc.org/github.com/gorilla/websocket) +* [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc) * [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat) * [Command example](https://github.com/gorilla/websocket/tree/master/examples/command) * [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo) diff --git a/vendor/github.com/gorilla/websocket/conn.go b/vendor/github.com/gorilla/websocket/conn.go index 6f17cd29982..ca46d2f793c 100644 --- a/vendor/github.com/gorilla/websocket/conn.go +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -244,8 +244,8 @@ type Conn struct { subprotocol string // Write fields - mu chan bool // used as mutex to protect write to conn - writeBuf []byte // frame is constructed in this buffer. + mu chan struct{} // used as mutex to protect write to conn + writeBuf []byte // frame is constructed in this buffer. writePool BufferPool writeBufSize int writeDeadline time.Time @@ -302,8 +302,8 @@ func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBuf = make([]byte, writeBufferSize) } - mu := make(chan bool, 1) - mu <- true + mu := make(chan struct{}, 1) + mu <- struct{}{} c := &Conn{ isServer: isServer, br: br, @@ -377,7 +377,7 @@ func (c *Conn) read(n int) ([]byte, error) { func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error { <-c.mu - defer func() { c.mu <- true }() + defer func() { c.mu <- struct{}{} }() c.writeErrMu.Lock() err := c.writeErr @@ -429,7 +429,7 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er maskBytes(key, 0, buf[6:]) } - d := time.Hour * 1000 + d := 1000 * time.Hour if !deadline.IsZero() { d = deadline.Sub(time.Now()) if d < 0 { @@ -444,7 +444,7 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er case <-timer.C: return errWriteTimeout } - defer func() { c.mu <- true }() + defer func() { c.mu <- struct{}{} }() c.writeErrMu.Lock() err := c.writeErr diff --git a/vendor/github.com/gorilla/websocket/doc.go b/vendor/github.com/gorilla/websocket/doc.go index c6f4df8960f..8db0cef95a2 100644 --- a/vendor/github.com/gorilla/websocket/doc.go +++ b/vendor/github.com/gorilla/websocket/doc.go @@ -187,9 +187,9 @@ // than the largest message do not provide any benefit. // // Depending on the distribution of message sizes, setting the buffer size to -// 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 +// 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%. // diff --git a/vendor/github.com/gorilla/websocket/prepared.go b/vendor/github.com/gorilla/websocket/prepared.go index 74ec565d2c3..c854225e967 100644 --- a/vendor/github.com/gorilla/websocket/prepared.go +++ b/vendor/github.com/gorilla/websocket/prepared.go @@ -73,8 +73,8 @@ func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) { // 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 + mu := make(chan struct{}, 1) + mu <- struct{}{} var nc prepareConn c := &Conn{ conn: &nc, diff --git a/vendor/modules.txt b/vendor/modules.txt index 1ccd82cb52f..6c13d4d2c7b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -466,6 +466,9 @@ github.com/golang/protobuf/ptypes/timestamp # github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db ## explicit github.com/golang/snappy +# github.com/gomodule/redigo v1.8.4 +## explicit; go 1.14 +github.com/gomodule/redigo/redis # github.com/google/btree v1.0.1 ## explicit; go 1.12 github.com/google/btree @@ -490,24 +493,26 @@ github.com/googleapis/gax-go/v2 github.com/googleapis/gnostic/compiler github.com/googleapis/gnostic/extensions github.com/googleapis/gnostic/openapiv2 -# github.com/googollee/go-engine.io v0.0.0-20180829091931-e2f255711dcb -## explicit -github.com/googollee/go-engine.io -github.com/googollee/go-engine.io/message -github.com/googollee/go-engine.io/parser -github.com/googollee/go-engine.io/polling -github.com/googollee/go-engine.io/transport -github.com/googollee/go-engine.io/websocket -# github.com/googollee/go-socket.io v0.0.0-20181214084611-0ad7206c347a -## explicit +# github.com/googollee/go-socket.io v1.7.0 +## explicit; go 1.16 github.com/googollee/go-socket.io +github.com/googollee/go-socket.io/engineio +github.com/googollee/go-socket.io/engineio/frame +github.com/googollee/go-socket.io/engineio/packet +github.com/googollee/go-socket.io/engineio/payload +github.com/googollee/go-socket.io/engineio/session +github.com/googollee/go-socket.io/engineio/transport +github.com/googollee/go-socket.io/engineio/transport/polling +github.com/googollee/go-socket.io/engineio/transport/utils +github.com/googollee/go-socket.io/engineio/transport/websocket +github.com/googollee/go-socket.io/parser # github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 ## explicit github.com/gopherjs/gopherjs/js # github.com/gorilla/mux v1.7.0 ## explicit github.com/gorilla/mux -# github.com/gorilla/websocket v1.4.1 +# github.com/gorilla/websocket v1.4.2 ## explicit; go 1.12 github.com/gorilla/websocket # github.com/gosuri/uitable v0.0.0-20160404203958-36ee7e946282