diff --git a/Gopkg.lock b/Gopkg.lock index 7d5ba4f508..cbf2c79e3e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -352,6 +352,53 @@ revision = "7c663266750e7d82587642f65e60bc4083f1f84e" version = "v0.2.0" +[[projects]] + branch = "master" + digest = "1:324591cd9122f94cc39a87bd44ec71153672c239e2cff5743bfad96b60be3749" + name = "github.com/googollee/go-engine.io" + packages = [ + ".", + "message", + "parser", + "polling", + "transport", + "websocket", + ] + pruneopts = "UT" + revision = "e2f255711dcb2ea41e42c6d9d98c172d7edf6509" + +[[projects]] + branch = "master" + digest = "1:62db7727ec8e026e51e1b46696e0fdf6a956a701032eca4d63e311816820717c" + name = "github.com/googollee/go-socket.io" + packages = ["."] + pruneopts = "UT" + revision = "f12da5711bc66c18a0724987fff7f4f3db35c70f" + +[[projects]] + digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1" + name = "github.com/gorilla/context" + packages = ["."] + pruneopts = "UT" + revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42" + version = "v1.1.1" + +[[projects]] + digest = "1:e73f5b0152105f18bc131fba127d9949305c8693f8a762588a82a48f61756f5f" + name = "github.com/gorilla/mux" + packages = ["."] + pruneopts = "UT" + revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" + version = "v1.6.2" + +[[projects]] + digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d" + name = "github.com/gorilla/websocket" + packages = ["."] + pruneopts = "UT" + revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d" + version = "v1.4.0" + [[projects]] branch = "master" digest = "1:4e08dc2383a46b3107f0b34ca338c4459e8fc8ee90e46a60e728aa8a2b21d558" @@ -442,6 +489,14 @@ pruneopts = "UT" revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0" +[[projects]] + branch = "master" + digest = "1:094e04c4742b596bce4455eb535d4e4365016b44016db76f8593211406d77299" + name = "github.com/kr/pty" + packages = ["."] + pruneopts = "UT" + revision = "fa756f09eeb418bf1cc6268c66ceaad9bb98f598" + [[projects]] digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67" name = "github.com/mattn/go-colorable" @@ -1150,11 +1205,14 @@ "github.com/fatih/color", "github.com/go-sql-driver/mysql", "github.com/golang-plus/uuid", + "github.com/googollee/go-socket.io", + "github.com/gorilla/mux", "github.com/gosuri/uitable", "github.com/hako/durafmt", "github.com/jinzhu/gorm", "github.com/jinzhu/gorm/dialects/mysql", "github.com/json-iterator/go", + "github.com/kr/pty", "github.com/mholt/caddy", "github.com/mholt/caddy/startupshutdown", "github.com/miekg/dns", diff --git a/Gopkg.toml b/Gopkg.toml index 1762493a14..3353f32046 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -85,3 +85,11 @@ [[constraint]] branch = "master" name = "github.com/hako/durafmt" + +[[constraint]] + branch = "master" + name = "github.com/googollee/go-socket.io" + +[[constraint]] + branch = "master" + name = "github.com/kr/pty" diff --git a/build/webconsole/root/etc/yunion/webconsole.conf.sample b/build/webconsole/root/etc/yunion/webconsole.conf.sample new file mode 100644 index 0000000000..2f7b688105 --- /dev/null +++ b/build/webconsole/root/etc/yunion/webconsole.conf.sample @@ -0,0 +1,11 @@ +region = 'LzxHQ' +address = '10.168.222.245' +port = 8899 +auth_uri = 'http://10.168.222.245:35357/v3' +admin_user = 'regionadmin' +admin_password = 'iygPtGX6r7NZ_tW_' +admin_tenant_name = 'system' + +api_server = "http://webconsole.yunion.io" + +log_level = "debug" diff --git a/build/webconsole/vars b/build/webconsole/vars new file mode 100644 index 0000000000..e70015cc2c --- /dev/null +++ b/build/webconsole/vars @@ -0,0 +1 @@ +DESCRIPTION="Yunion Cloud Webconsole Service" diff --git a/cmd/climc/shell/k8s/pods.go b/cmd/climc/shell/k8s/pods.go index 5376147feb..5fc854b7c9 100644 --- a/cmd/climc/shell/k8s/pods.go +++ b/cmd/climc/shell/k8s/pods.go @@ -3,6 +3,8 @@ package k8s import ( "fmt" + "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/modules/k8s" ) @@ -28,6 +30,23 @@ func initPod() { return nil }) + type getOpt struct { + resourceGetOptions + } + R(&getOpt{}, cmdN("show"), "Get pod details", func(s *mcclient.ClientSession, args *getOpt) error { + id := args.NAME + params := args.ClusterParams() + if args.Namespace != "" { + params.Add(jsonutils.NewString(args.Namespace), "namespace") + } + ret, err := k8s.Pods.Get(s, id, params) + if err != nil { + return err + } + printObjectYAML(ret) + return nil + }) + type deleteOpt struct { resourceGetOptions } diff --git a/cmd/climc/shell/webconsole.go b/cmd/climc/shell/webconsole.go new file mode 100644 index 0000000000..6007362c47 --- /dev/null +++ b/cmd/climc/shell/webconsole.go @@ -0,0 +1,59 @@ +package shell + +import ( + "fmt" + "net/url" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modules" + o "yunion.io/x/onecloud/pkg/mcclient/options" +) + +func init() { + handleResult := func(opt o.WebConsoleFrontendOptions, obj jsonutils.JSONObject) error { + if opt.WebUrl == "" { + printObject(obj) + return nil + } + u, err := url.Parse(opt.WebUrl) + if err != nil { + return err + } + connParams, err := obj.GetString("connect_params") + if err != nil { + return err + } + u.Path = "index.html" + u.RawQuery = connParams + fmt.Println(u.String()) + return nil + } + + R(&o.PodShellOptions{}, "webconsole-k8s-pod", "Show TTY console of given pod", func(s *mcclient.ClientSession, args *o.PodShellOptions) error { + params, err := args.Params() + if err != nil { + return err + } + ret, err := modules.WebConsole.DoK8sShellConnect(s, args.NAME, params) + if err != nil { + return err + } + handleResult(args.WebConsoleFrontendOptions, ret) + return nil + }) + + R(&o.PodLogOptoins{}, "webconsole-k8s-pod-log", "Get logs of given pod", func(s *mcclient.ClientSession, args *o.PodLogOptoins) error { + params, err := args.Params() + if err != nil { + return err + } + ret, err := modules.WebConsole.DoK8sLogConnect(s, args.NAME, params) + if err != nil { + return err + } + handleResult(args.WebConsoleFrontendOptions, ret) + return nil + }) +} diff --git a/cmd/webconsole/main.go b/cmd/webconsole/main.go new file mode 100644 index 0000000000..46c760e115 --- /dev/null +++ b/cmd/webconsole/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "yunion.io/x/onecloud/pkg/webconsole/service" +) + +func main() { + service.StartService() +} diff --git a/pkg/compute/handlers.go b/pkg/compute/handlers.go index 2cf6dc6115..439e415b09 100644 --- a/pkg/compute/handlers.go +++ b/pkg/compute/handlers.go @@ -2,9 +2,9 @@ package compute import ( "yunion.io/x/log" + "yunion.io/x/onecloud/pkg/appsrv" "yunion.io/x/onecloud/pkg/appsrv/dispatcher" - "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/quotas" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" diff --git a/pkg/mcclient/modules/mod_webconsole.go b/pkg/mcclient/modules/mod_webconsole.go new file mode 100644 index 0000000000..6ff9db80ab --- /dev/null +++ b/pkg/mcclient/modules/mod_webconsole.go @@ -0,0 +1,65 @@ +package modules + +import ( + "fmt" + + "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/mcclient" +) + +var ( + WebConsole WebConsoleManager +) + +func init() { + WebConsole = WebConsoleManager{NewWebConsoleManager()} +} + +type WebConsoleManager struct { + ResourceManager +} + +func NewWebConsoleManager() ResourceManager { + return ResourceManager{BaseManager: BaseManager{serviceType: "webconsole"}, + Keyword: "webconsole", KeywordPlural: "webconsole"} +} + +func (m WebConsoleManager) DoConnect( + s *mcclient.ClientSession, + connType, id, action string, + params jsonutils.JSONObject, +) (jsonutils.JSONObject, error) { + if len(connType) == 0 { + return nil, fmt.Errorf("Empty connection resource type") + } + url := fmt.Sprintf("/webconsole/%s", connType) + if id != "" { + url = fmt.Sprintf("%s/%s", url, id) + } + if action != "" { + url = fmt.Sprintf("%s/%s", url, action) + } + return m._post(s, url, params, "webconsole") +} + +func (m WebConsoleManager) DoK8sConnect( + s *mcclient.ClientSession, + id, action string, + params jsonutils.JSONObject, +) (jsonutils.JSONObject, error) { + return m.DoConnect(s, "k8s", id, action, params) +} + +func (m WebConsoleManager) DoK8sShellConnect( + s *mcclient.ClientSession, + id string, params jsonutils.JSONObject, +) (jsonutils.JSONObject, error) { + return m.DoK8sConnect(s, id, "shell", params) +} + +func (m WebConsoleManager) DoK8sLogConnect( + s *mcclient.ClientSession, + id string, params jsonutils.JSONObject, +) (jsonutils.JSONObject, error) { + return m.DoK8sConnect(s, id, "log", params) +} diff --git a/pkg/mcclient/options/webconsole.go b/pkg/mcclient/options/webconsole.go new file mode 100644 index 0000000000..ce5cfb06c9 --- /dev/null +++ b/pkg/mcclient/options/webconsole.go @@ -0,0 +1,29 @@ +package options + +import ( + "yunion.io/x/jsonutils" +) + +type WebConsoleFrontendOptions struct { + WebUrl string `help:"Frontend terminal HTML web url" short-token:"w"` +} + +type PodBaseOptions struct { + WebConsoleFrontendOptions + NAME string `help:"Name of k8s pod to connect"` + Namespace string `help:"Namespace of this pod"` + Container string `help:"Container in this pod"` + Cluster string `default:"$K8S_CLUSTER|default" help:"Kubernetes cluster name"` +} + +func (opt *PodBaseOptions) Params() (*jsonutils.JSONDict, error) { + return StructToParams(opt) +} + +type PodShellOptions struct { + PodBaseOptions +} + +type PodLogOptoins struct { + PodBaseOptions +} diff --git a/pkg/webconsole/command/command.go b/pkg/webconsole/command/command.go new file mode 100644 index 0000000000..35ed12f53d --- /dev/null +++ b/pkg/webconsole/command/command.go @@ -0,0 +1,46 @@ +package command + +import ( + "os/exec" + + "yunion.io/x/log" +) + +const ( + PROTOCOL_TTY string = "tty" + //PROTOCOL_VNC string = "vnc" +) + +type ICommand interface { + GetProtocol() string + GetCommand() *exec.Cmd + Cleanup() error +} + +type BaseCommand struct { + name string + args []string +} + +func NewBaseCommand(name string, args ...string) *BaseCommand { + return &BaseCommand{ + name: name, + args: args, + } +} + +func (c *BaseCommand) AppendArgs(args ...string) *BaseCommand { + for _, arg := range args { + c.args = append(c.args, arg) + } + return c +} + +func (c BaseCommand) GetCommand() *exec.Cmd { + return exec.Command(c.name, c.args...) +} + +func (c BaseCommand) Cleanup() error { + log.Infof("BaseCommand Cleanup do nothing") + return nil +} diff --git a/pkg/webconsole/command/kube_command.go b/pkg/webconsole/command/kube_command.go new file mode 100644 index 0000000000..34a9087a6d --- /dev/null +++ b/pkg/webconsole/command/kube_command.go @@ -0,0 +1,127 @@ +package command + +import ( + "fmt" + "os" + "os/exec" + + "yunion.io/x/log" +) + +type Kubectl struct { + *BaseCommand + kubeconfig string +} + +func NewKubectlCommand(kubeconfig, namespace string) *Kubectl { + name := "kubectl" + if len(namespace) == 0 { + namespace = "default" + } + cmd := NewBaseCommand(name, "--namespace", namespace) + return &Kubectl{ + BaseCommand: cmd, + kubeconfig: kubeconfig, + } +} + +func (c *Kubectl) GetCommand() *exec.Cmd { + cmd := c.BaseCommand.GetCommand() + cmd.Env = append(cmd.Env, fmt.Sprintf("KUBECONFIG=%s", c.kubeconfig)) + return cmd +} + +func (c Kubectl) GetProtocol() string { + return PROTOCOL_TTY +} + +func (c *Kubectl) Cleanup() error { + log.Debugf("Remove temp kubeconfig file: %s", c.kubeconfig) + return os.Remove(c.kubeconfig) +} + +type KubectlExec struct { + *Kubectl +} + +func (c *Kubectl) Exec() *KubectlExec { + // Execute a command in a container + cmd := &KubectlExec{ + Kubectl: c, + } + cmd.AppendArgs("exec") + return cmd +} + +func (c *KubectlExec) Stdin() *KubectlExec { + // -i: Pass stdin to the container + c.AppendArgs("-i") + return c +} + +func (c *KubectlExec) TTY() *KubectlExec { + // -t: Stdin is a TTY + c.AppendArgs("-t") + return c +} + +func (c *KubectlExec) Container(name string) *KubectlExec { + if len(name) == 0 { + return c + } + // -c: Container name. If ommitted, the first container in the pod will be chosen + c.AppendArgs("-c", name) + return c +} + +func (c *KubectlExec) Pod(name string) *KubectlExec { + // Pod name + c.AppendArgs(name) + return c +} + +func (c *KubectlExec) Command(cmd string, args ...string) *KubectlExec { + c.AppendArgs("--", cmd) + c.AppendArgs(args...) + return c +} + +func NewPodBashCommand(kubeconfig, namespace, pod, container string) ICommand { + return NewKubectlCommand(kubeconfig, namespace).Exec(). + Stdin(). + TTY(). + Pod(pod). + Container(container). + Command("bash", "-i", "-l") +} + +type KubectlLog struct { + *Kubectl +} + +func (c *Kubectl) Logs() *KubectlLog { + // Print the logs for a container in a pod + cmd := &KubectlLog{ + Kubectl: c, + } + cmd.AppendArgs("logs") + return cmd +} + +func (c *KubectlLog) Follow() *KubectlLog { + // -f: Specify if the logs should be streamed + c.AppendArgs("-f") + return c +} + +func (c *KubectlLog) Pod(name string) *KubectlLog { + // Pod name + c.AppendArgs(name) + return c +} + +func NewPodLogCommand(kubeconfig, namespace, pod, container string) ICommand { + return NewKubectlCommand(kubeconfig, namespace).Logs(). + Follow(). + Pod(pod) +} diff --git a/pkg/webconsole/command/kube_command_test.go b/pkg/webconsole/command/kube_command_test.go new file mode 100644 index 0000000000..2aa28f4307 --- /dev/null +++ b/pkg/webconsole/command/kube_command_test.go @@ -0,0 +1,51 @@ +package command + +import ( + "reflect" + "strings" + "testing" +) + +func TestKubectlExec_Command(t *testing.T) { + type fields struct { + Kubectl *Kubectl + } + type args struct { + cmd string + args []string + } + tests := []struct { + name string + fields fields + args args + want string + }{ + { + name: "bash command", + fields: fields{ + Kubectl: NewKubectlCommand("/tmp/kubeconfig", "system"), + }, + args: args{ + cmd: "bash", + args: []string{"-il"}, + }, + want: "kubectl --namespace system exec -i -t Pod1 -c Container1 -- bash -il", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := tt.fields.Kubectl.Exec(). + Stdin().TTY(). + Pod("Pod1"). + Container("Container1") + cmd := c.Command(tt.args.cmd, tt.args.args...) + name := cmd.name + args := cmd.args + got := []string{name} + got = append(got, args...) + if !reflect.DeepEqual(strings.Join(got, " "), tt.want) { + t.Errorf("KubectlExec.Command() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/webconsole/handlers.go b/pkg/webconsole/handlers.go new file mode 100644 index 0000000000..aebe822773 --- /dev/null +++ b/pkg/webconsole/handlers.go @@ -0,0 +1,151 @@ +package webconsole + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/pkg/util/sets" + + "yunion.io/x/onecloud/pkg/appctx" + "yunion.io/x/onecloud/pkg/appsrv" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient/auth" + "yunion.io/x/onecloud/pkg/mcclient/modules/k8s" + "yunion.io/x/onecloud/pkg/webconsole/command" + o "yunion.io/x/onecloud/pkg/webconsole/options" + "yunion.io/x/onecloud/pkg/webconsole/session" +) + +const ( + ApiPathPrefix = "/webconsole/" + ConnectPathPrefix = "/connect/" +) + +func InitHandlers(app *appsrv.Application) { + app.AddHandler("POST", ApiPathPrefix+"k8s//shell", auth.Authenticate(handleK8sShell)) + app.AddHandler("POST", ApiPathPrefix+"k8s//log", auth.Authenticate(handleK8sLog)) +} + +func fetchEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (map[string]string, jsonutils.JSONObject, jsonutils.JSONObject) { + params := appctx.AppContextParams(ctx) + query, e := jsonutils.ParseQueryString(r.URL.RawQuery) + if e != nil { + log.Errorf("Parse query string %q failed: %v", r.URL.RawQuery, e) + } + var body jsonutils.JSONObject = nil + if sets.NewString("PUT", "POST", "DELETE", "PATCH").Has(r.Method) { + body, e = appsrv.FetchJSON(r) + if e != nil { + log.Errorf("Failed to decode JSON request body: %v", e) + } + } + return params, query, body +} + +type K8sEnv struct { + Cluster string + Namespace string + Pod string + Container string + KubeConfig string +} + +func fetchK8sEnv(ctx context.Context, w http.ResponseWriter, r *http.Request) (*K8sEnv, error) { + params, _, body := fetchEnv(ctx, w, r) + cluster, _ := body.GetString("cluster") + if cluster == "" { + cluster = "default" + } + namespace, _ := body.GetString("namespace") + if namespace == "" { + namespace = "default" + } + podName := params[""] + container, _ := body.GetString("container") + adminSession := auth.GetAdminSession(o.Options.Region, "") + + query := jsonutils.NewDict() + query.Add(jsonutils.NewString(namespace), "namespace") + query.Add(jsonutils.NewString(cluster), "cluster") + obj, err := k8s.Pods.Get(adminSession, podName, query) + if err != nil { + return nil, err + } + if obj == nil { + return nil, httperrors.NewNotFoundError("Not found pod %q", podName) + } + + data := jsonutils.NewDict() + data.Add(jsonutils.JSONTrue, "directly") + ret, err := k8s.Clusters.PerformAction(adminSession, cluster, "generate-kubeconfig", data) + if err != nil { + return nil, err + } + conf, err := ret.GetString("kubeconfig") + if err != nil { + return nil, httperrors.NewNotFoundError("Not found cluster %q kubeconfig", cluster) + } + f, err := ioutil.TempFile("", "kubeconfig-") + if err != nil { + return nil, fmt.Errorf("Save kubeconfig error: %v", err) + } + f.WriteString(conf) + defer f.Close() + + return &K8sEnv{ + Cluster: cluster, + Namespace: namespace, + Pod: podName, + Container: container, + KubeConfig: f.Name(), + }, nil +} + +func handleK8sCommand( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + cmdFactory func(kubeconfig, namespace, pod, container string) command.ICommand, +) { + env, err := fetchK8sEnv(ctx, w, r) + if err != nil { + httperrors.GeneralServerError(w, err) + return + } + + cmd := cmdFactory(env.KubeConfig, env.Namespace, env.Pod, env.Container) + cmdSession, err := session.Manager.Save(cmd) + if err != nil { + httperrors.GeneralServerError(w, err) + return + } + data := jsonutils.NewDict() + params, err := cmdSession.GetConnectParams() + if err != nil { + httperrors.GeneralServerError(w, err) + return + } + data.Add(jsonutils.NewString(params), "connect_params") + data.Add(jsonutils.NewString(cmdSession.Id), "session") + sendJSON(w, data) +} + +func handleK8sShell(ctx context.Context, w http.ResponseWriter, r *http.Request) { + handleK8sCommand(ctx, w, r, command.NewPodBashCommand) +} + +func handleK8sLog(ctx context.Context, w http.ResponseWriter, r *http.Request) { + handleK8sCommand(ctx, w, r, command.NewPodLogCommand) +} + +func sendJSON(w http.ResponseWriter, body jsonutils.JSONObject) { + ret := jsonutils.NewDict() + if body != nil { + ret.Add(body, "webconsole") + } + appsrv.SendJSON(w, ret) +} diff --git a/pkg/webconsole/options/options.go b/pkg/webconsole/options/options.go new file mode 100644 index 0000000000..7561ce79eb --- /dev/null +++ b/pkg/webconsole/options/options.go @@ -0,0 +1,15 @@ +package options + +import ( + "yunion.io/x/onecloud/pkg/cloudcommon" +) + +var ( + Options WebConsoleOptions +) + +type WebConsoleOptions struct { + cloudcommon.Options + + ApiServer string `help:"API server url to handle websocket connection, usually with public access" default:"http://webconsole.yunion.io"` +} diff --git a/pkg/webconsole/server/server.go b/pkg/webconsole/server/server.go new file mode 100644 index 0000000000..04796caf70 --- /dev/null +++ b/pkg/webconsole/server/server.go @@ -0,0 +1,45 @@ +package server + +import ( + "fmt" + "net/http" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/webconsole/session" +) + +type ConnectionServer struct { +} + +func NewConnectionServer() *ConnectionServer { + return &ConnectionServer{} +} + +func (s *ConnectionServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { + query, err := jsonutils.ParseQueryString(req.URL.RawQuery) + if err != nil { + httperrors.GeneralServerError(w, err) + return + } + log.Debugf("[connection] Get query: %v", query) + accessToken, _ := query.GetString("access_token") + if accessToken == "" { + httperrors.BadRequestError(w, fmt.Sprintf("Empty access_token")) + return + } + sessionObj, ok := session.Manager.Get(accessToken) + if !ok { + log.Warningf("Not found session by token: %q", accessToken) + httperrors.NotFoundError(w, fmt.Sprintf("Not found session")) + return + } + ttyServer, err := NewTTYServer(sessionObj) + if err != nil { + httperrors.GeneralServerError(w, fmt.Errorf("New TTY error: %v", err)) + return + } + ttyServer.ServeHTTP(w, req) +} diff --git a/pkg/webconsole/server/tty_server.go b/pkg/webconsole/server/tty_server.go new file mode 100644 index 0000000000..717f7d1fa4 --- /dev/null +++ b/pkg/webconsole/server/tty_server.go @@ -0,0 +1,109 @@ +package server + +import ( + socketio "github.com/googollee/go-socket.io" + "github.com/kr/pty" + + "yunion.io/x/log" + + "yunion.io/x/onecloud/pkg/webconsole/session" +) + +const ( + ON_CONNECTION = "connection" + ON_DISCONNECTION = "disconnection" + ON_ERROR = "error" + + OUTPUT_EVENT = "output" + INPUT_EVENT = "input" + RESIZE_EVENT = "resize" + + COMMAND_QUERY = "command" + ARGS_QUERY = "args" +) + +type TTYServer struct { + *socketio.Server +} + +func NewTTYServer(s *session.SSession) (*TTYServer, error) { + socketioServer, err := socketio.NewServer(nil) + if err != nil { + return nil, err + } + server := &TTYServer{ + Server: socketioServer, + } + server.initEventHandler(s) + return server, nil +} + +func (server *TTYServer) initEventHandler(s *session.SSession) { + server.On(ON_CONNECTION, func(so socketio.Socket) error { + log.Infof("[%q] On connection", so.Id()) + p, err := session.NewPty(s) + if err != nil { + log.Errorf("Create Pty error: %v", err) + return err + } + initSocketHandler(so, p) + return nil + }) +} + +func initSocketHandler(so socketio.Socket, p *session.Pty) { + // handle read + go func() { + buf := make([]byte, 1024) + for { + n, err := p.Pty.Read(buf) + if err != nil { + log.Errorf("Failed to read from pty master: %v", err) + cleanUp(so, p) + return + } + so.Emit(OUTPUT_EVENT, string(buf[0:n])) + } + }() + + // handle write + so.On(INPUT_EVENT, func(data string) { + p.Pty.Write([]byte(data)) + }) + + // handle resize + so.On(RESIZE_EVENT, func(colRow []uint16) { + if len(colRow) != 2 { + log.Errorf("Invalid window size: %v", colRow) + cleanUp(so, p) + return + } + //size, err := pty.GetsizeFull(p.Pty) + //if err != nil { + //log.Errorf("Get pty window size error: %v", err) + //return + //} + newSize := pty.Winsize{ + Cols: colRow[0], + Rows: colRow[1], + } + p.Resize(&newSize) + }) + + // handle disconnection + so.On(ON_DISCONNECTION, func(msg string) { + log.Infof("[%s] closed: %s", so.Id(), msg) + cleanUp(so, p) + }) + + // handle error + so.On(ON_ERROR, func(err error) { + log.Errorf("[%s] on error: %v", so.Id(), err) + cleanUp(so, p) + }) +} + +func cleanUp(so socketio.Socket, p *session.Pty) { + so.Disconnect() + p.Stop() +} diff --git a/pkg/webconsole/service/service.go b/pkg/webconsole/service/service.go new file mode 100644 index 0000000000..29dc081d04 --- /dev/null +++ b/pkg/webconsole/service/service.go @@ -0,0 +1,56 @@ +package service + +import ( + "net" + "net/http" + "net/url" + "os" + "strconv" + + "github.com/gorilla/mux" + + "yunion.io/x/log" + + "yunion.io/x/onecloud/pkg/cloudcommon" + "yunion.io/x/onecloud/pkg/webconsole" + o "yunion.io/x/onecloud/pkg/webconsole/options" + "yunion.io/x/onecloud/pkg/webconsole/server" +) + +func StartService() { + cloudcommon.ParseOptions(&o.Options, &o.Options.Options, os.Args, "webconsole.conf") + + if o.Options.ApiServer == "" { + log.Fatalf("--api-server must specified") + } + _, err := url.Parse(o.Options.ApiServer) + if err != nil { + log.Fatalf("invalid --api-server %s", o.Options.ApiServer) + } + + cloudcommon.InitAuth(&o.Options.Options, func() { + log.Infof("Auth complete") + }) + start() +} + +func start() { + app := cloudcommon.InitApp(&o.Options.Options) + webconsole.InitHandlers(app) + + root := mux.NewRouter() + root.UseEncodedPath() + + // api handler + root.PathPrefix(webconsole.ApiPathPrefix).Handler(app) + + // websocket related console handler + root.Handle(webconsole.ConnectPathPrefix, server.NewConnectionServer()) + + addr := net.JoinHostPort(o.Options.Address, strconv.Itoa(o.Options.Port)) + log.Infof("Start listen on %s", addr) + err := http.ListenAndServe(addr, root) + if err != nil { + log.Fatalf("%v", err) + } +} diff --git a/pkg/webconsole/session/pty_session.go b/pkg/webconsole/session/pty_session.go new file mode 100644 index 0000000000..c77f7f5600 --- /dev/null +++ b/pkg/webconsole/session/pty_session.go @@ -0,0 +1,71 @@ +package session + +import ( + "os" + "os/exec" + "os/signal" + "syscall" + + "github.com/kr/pty" + + "yunion.io/x/log" +) + +type Pty struct { + Session *SSession + Cmd *exec.Cmd + Pty *os.File + sizeCh chan os.Signal + size *pty.Winsize +} + +func NewPty(session *SSession) (p *Pty, err error) { + cmd := session.GetCommand() + p = &Pty{ + Session: session, + Cmd: cmd, + } + p.Pty, err = pty.Start(p.Cmd) + if err != nil { + return + } + p.sizeCh = make(chan os.Signal, 1) + p.size = &pty.Winsize{} + p.startResizeMonitor() + signal.Notify(p.sizeCh, syscall.SIGWINCH) // Initail resize + return +} + +func (p *Pty) startResizeMonitor() { + go func() { + for range p.sizeCh { + if err := pty.Setsize(p.Pty, p.size); err != nil { + log.Errorf("Resize pty error: %v", err) + } else { + log.Debugf("Resize pty to %#v, cmd: %#v", p.size, p.Cmd) + } + } + }() +} + +func (p *Pty) Resize(size *pty.Winsize) { + p.size = size + p.sizeCh <- syscall.SIGWINCH +} + +func (p *Pty) Stop() { + var err error + err = p.Pty.Close() + if err != nil { + log.Errorf("Close PTY error: %v", err) + } + err = p.Cmd.Process.Signal(os.Kill) + if err != nil { + log.Errorf("Kill command process error: %v", err) + } + err = p.Cmd.Wait() + if err != nil { + log.Errorf("Wait command error: %v", err) + } + p.Session.Close() +} diff --git a/pkg/webconsole/session/session.go b/pkg/webconsole/session/session.go new file mode 100644 index 0000000000..b790004a1d --- /dev/null +++ b/pkg/webconsole/session/session.go @@ -0,0 +1,92 @@ +package session + +import ( + "fmt" + "math/rand" + "net/url" + "sync" + + "github.com/golang-plus/uuid" + + "yunion.io/x/log" + "yunion.io/x/pkg/utils" + + "yunion.io/x/onecloud/pkg/webconsole/command" + o "yunion.io/x/onecloud/pkg/webconsole/options" +) + +var ( + Manager *SSessionManager + AES_KEY string +) + +func init() { + Manager = NewSessionManager() + AES_KEY = fmt.Sprintf("webconsole-%f", rand.Float32()) +} + +type SSessionManager struct { + *sync.Map +} + +func NewSessionManager() *SSessionManager { + s := &SSessionManager{ + Map: &sync.Map{}, + } + return s +} + +func (man *SSessionManager) Save(Command command.ICommand) (session *SSession, err error) { + key, err := uuid.NewV4() + if err != nil { + return + } + idStr := key.String() + token, err := utils.EncryptAESBase64Url(AES_KEY, idStr) + if err != nil { + return + } + session = &SSession{ + Id: idStr, + ICommand: Command, + AccessToken: token, + } + man.Store(idStr, session) + return +} + +func (man *SSessionManager) Get(accessToken string) (*SSession, bool) { + id, err := utils.DescryptAESBase64Url(AES_KEY, accessToken) + if err != nil { + log.Errorf("DescryptAESBase64Url error: %v", err) + return nil, false + } + obj, ok := man.Load(id) + if !ok { + return nil, false + } + return obj.(*SSession), true +} + +type SSession struct { + command.ICommand + Id string + AccessToken string +} + +func (s SSession) GetConnectParams() (string, error) { + params := url.Values{ + "api_server": {o.Options.ApiServer}, + "access_token": {s.AccessToken}, + "protocol": {s.GetProtocol()}, + } + return params.Encode(), nil +} + +func (s *SSession) Close() error { + if err := s.ICommand.Cleanup(); err != nil { + log.Errorf("Clean up command error: %v", err) + } + Manager.Delete(s.Id) + return nil +} diff --git a/vendor/github.com/googollee/go-engine.io/.travis.yml b/vendor/github.com/googollee/go-engine.io/.travis.yml new file mode 100644 index 0000000000..0a5134f0dc --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/.travis.yml @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000000..04a4430216 --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/LICENSE @@ -0,0 +1,23 @@ +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/README.md b/vendor/github.com/googollee/go-engine.io/README.md new file mode 100644 index 0000000000..c5d603f572 --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/README.md @@ -0,0 +1,78 @@ +# 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) + +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). + +It is compatible with node.js implement, and supported long-polling and websocket transport. + +## Install + +Install the package with: + +```bash +go get github.com/googollee/go-engine.io +``` + +Import it with: + +```go +import "github.com/googollee/go-engine.io" +``` + +and use `engineio` as the package name inside the code. + +## Example + +Please check example folder for details. + +```go +package main + +import ( + "encoding/hex" + "io/ioutil" + "log" + "net/http" + + "github.com/googollee/go-engine.io" +) + +func main() { + server, err := engineio.NewServer(nil) + if err != nil { + log.Fatal(err) + } + + go func() { + for { + conn, _ := server.Accept() + go func() { + defer conn.Close() + for i := 0; i < 10; i++ { + 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.Close() + } + }() + } + }() + + 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 diff --git a/vendor/github.com/googollee/go-engine.io/ioutil.go b/vendor/github.com/googollee/go-engine.io/ioutil.go new file mode 100644 index 0000000000..7b933de8da --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/ioutil.go @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000000..6ff50b6460 --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/message/message.go @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000000..f0e1264d3c --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/parser/limit_reader.go @@ -0,0 +1,45 @@ +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 new file mode 100644 index 0000000000..be3f78a4f8 --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/parser/packet.go @@ -0,0 +1,191 @@ +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 new file mode 100644 index 0000000000..b55076248f --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/parser/parser.go @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000000..41fd1c6ced --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/parser/payload.go @@ -0,0 +1,170 @@ +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 new file mode 100644 index 0000000000..842a715d8a --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/polling/client.go @@ -0,0 +1,149 @@ +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 new file mode 100644 index 0000000000..7de1acbc2f --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/polling/server.go @@ -0,0 +1,197 @@ +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 new file mode 100644 index 0000000000..28f0ad3851 --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/polling/try_locker.go @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000000..2c81b5c339 --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/polling/writer.go @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000000..d86a51ff4c --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/polling/xhr.go @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000000..24a14206db --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/server.go @@ -0,0 +1,189 @@ +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 new file mode 100644 index 0000000000..ae85459fd1 --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/server_conn.go @@ -0,0 +1,388 @@ +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 new file mode 100644 index 0000000000..6b1daa9645 --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/sessions.go @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000000..e549a3082e --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/transport/transport.go @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000000..eeb2cc27b9 --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/websocket/client.go @@ -0,0 +1,72 @@ +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 new file mode 100644 index 0000000000..d72103ff91 --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/websocket/server.go @@ -0,0 +1,81 @@ +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 new file mode 100644 index 0000000000..a573f4907f --- /dev/null +++ b/vendor/github.com/googollee/go-engine.io/websocket/websocket.go @@ -0,0 +1,12 @@ +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/.travis.yml b/vendor/github.com/googollee/go-socket.io/.travis.yml new file mode 100644 index 0000000000..7141eabd29 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/.travis.yml @@ -0,0 +1,5 @@ +language: go +go: 1.5 +install: + - go get "github.com/smartystreets/goconvey/convey" + - go get -v . diff --git a/vendor/github.com/googollee/go-socket.io/LICENSE b/vendor/github.com/googollee/go-socket.io/LICENSE new file mode 100644 index 0000000000..04a4430216 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/LICENSE @@ -0,0 +1,23 @@ +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-socket.io/README.md b/vendor/github.com/googollee/go-socket.io/README.md new file mode 100644 index 0000000000..e5a160ddd0 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/README.md @@ -0,0 +1,127 @@ +# 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) + +**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 an implementation of [socket.io](http://socket.io) in golang, which is a realtime application framework. + +It is compatible with latest implementation of socket.io in node.js, and supports room and namespace. + +* for compatability with socket.io 0.9.x, please use branch 0.9.x * + +## Install + +Install the package with: + +```bash +go get github.com/googollee/go-socket.io +``` + +Import it with: + +```go +import "github.com/googollee/go-socket.io" +``` + +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)) + so.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 + +```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" +}) +``` + +##### Sending ACK with data from CLIENT to SERVER + +* Client-side + +```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 +} +``` + +* Server-side + +```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) +}) + +// Or + +so.Emit("some:event", dataForClient, func (so socketio.Socket, data string) { + log.Println("Client ACK with data: ", data) +}) +``` + +## License + +The 3-clause BSD License - see LICENSE 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 new file mode 100644 index 0000000000..32b1ae8891 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/adapter.go @@ -0,0 +1,79 @@ +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/attachment.go b/vendor/github.com/googollee/go-socket.io/attachment.go new file mode 100644 index 0000000000..2f04c08233 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/attachment.go @@ -0,0 +1,168 @@ +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/caller.go b/vendor/github.com/googollee/go-socket.io/caller.go new file mode 100644 index 0000000000..15666b440d --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/caller.go @@ -0,0 +1,82 @@ +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/handler.go b/vendor/github.com/googollee/go-socket.io/handler.go new file mode 100644 index 0000000000..b6a689d557 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/handler.go @@ -0,0 +1,219 @@ +package socketio + +import ( + "fmt" + "reflect" + "sync" +) + +type baseHandler struct { + events map[string]*caller + name string + broadcast BroadcastAdaptor + evMu sync.Mutex +} + +func newBaseHandler(name string, broadcast BroadcastAdaptor) *baseHandler { + return &baseHandler{ + events: make(map[string]*caller), + name: name, + broadcast: broadcast, + evMu: sync.Mutex{}, + } +} + +// 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 +} + +type socketHandler struct { + *baseHandler + acksmu sync.Mutex + acks map[int]*caller + socket *socket + rooms map[string]struct{} +} + +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{}), + } +} + +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 (h *socketHandler) Rooms() []string { + ret := make([]string, len(h.rooms)) + i := 0 + for room := range h.rooms { + ret[i] = room + i++ + } + return ret +} + +func (h *socketHandler) Join(room string) error { + if err := h.baseHandler.broadcast.Join(h.broadcastName(room), h.socket); err != nil { + return err + } + 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 + } + 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 + } + } + 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) +} + +func (h *socketHandler) onPacket(decoder *decoder, packet *packet) ([]interface{}, error) { + defer func() { + if decoder != nil { + decoder.Close() + } + }() + + 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) + } + + retV := c.Call(h.socket, args) + if len(retV) == 0 { + return nil, nil + } + + 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() + } + 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 + } + delete(h.acks, id) + h.acksmu.Unlock() + + args := c.GetArgs() + packet.Data = &args + if err := decoder.DecodeData(packet); err != nil { + return err + } + c.Call(h.socket, args) + return nil +} diff --git a/vendor/github.com/googollee/go-socket.io/ioutil.go b/vendor/github.com/googollee/go-socket.io/ioutil.go new file mode 100644 index 0000000000..7767d4e408 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/ioutil.go @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000000..f01ed76d04 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/main.go @@ -0,0 +1,6 @@ +/* +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 new file mode 100644 index 0000000000..9feb0b4040 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/message_reader.go @@ -0,0 +1,60 @@ +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 new file mode 100644 index 0000000000..f2e1004e31 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/namespace.go @@ -0,0 +1,47 @@ +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/parser.go b/vendor/github.com/googollee/go-socket.io/parser.go new file mode 100644 index 0000000000..8555e6d612 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/parser.go @@ -0,0 +1,336 @@ +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/server.go b/vendor/github.com/googollee/go-socket.io/server.go new file mode 100644 index 0000000000..4e8d687c12 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/server.go @@ -0,0 +1,111 @@ +package socketio + +import ( + "github.com/googollee/go-engine.io" + "net/http" + "time" +) + +// Server is the server of socket.io. +type Server struct { + *namespace + broadcast BroadcastAdaptor + eio *engineio.Server +} + +// 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) + if err != nil { + return nil, err + } + ret := &Server{ + namespace: newNamespace(newBroadcastDefault()), + eio: eio, + } + go ret.loop() + return ret, nil +} + +// 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) +} + +// SetPingInterval sets the interval of pings. Default is 25s. +func (s *Server) SetPingInterval(t time.Duration) { + s.eio.SetPingInterval(t) +} + +// SetMaxConnection sets the maximum number of connections with clients. Default is 1000. +func (s *Server) SetMaxConnection(n int) { + s.eio.SetMaxConnection(n) +} + +// GetMaxConnection returns the current max connection +func (s *Server) GetMaxConnection() int { + return s.eio.GetMaxConnection() +} + +// Count returns the current number of connected clients in session +func (s *Server) Count() int { + return s.eio.Count() +} + +// LenRoom returns the current number of connected clients in room +func (s *Server) LenRoom(room string) int { + return s.namespace.broadcast.Len(room) +} + +// 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) +} + +// SetAllowUpgrades sets whether server allows transport upgrades. Default is true. +func (s *Server) SetAllowUpgrades(allow bool) { + s.eio.SetAllowUpgrades(allow) +} + +// SetCookie sets the name of the cookie used by engine.io. Default is "io". +func (s *Server) SetCookie(prefix string) { + s.eio.SetCookie(prefix) +} + +// 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) +} + +// 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) +} + +// SetAdaptor sets the adaptor of broadcast. Default is an in-process broadcast implementation. +func (s *Server) SetAdaptor(adaptor BroadcastAdaptor) { + s.namespace = newNamespace(adaptor) +} + +// ServeHTTP handles http requests. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.eio.ServeHTTP(w, r) +} + +// BroadcastTo is a server level broadcast function. +func (s *Server) BroadcastTo(room, message string, args ...interface{}) { + s.namespace.BroadcastTo(room, message, args...) +} + +func (s *Server) loop() { + for { + conn, err := s.eio.Accept() + if err != nil { + return + } + s := newSocket(conn, s.baseHandler) + go func(s *socket) { + s.loop() + }(s) + } +} diff --git a/vendor/github.com/googollee/go-socket.io/socket.go b/vendor/github.com/googollee/go-socket.io/socket.go new file mode 100644 index 0000000000..2c6fa48286 --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/socket.go @@ -0,0 +1,174 @@ +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 new file mode 100644 index 0000000000..b6f3880eef --- /dev/null +++ b/vendor/github.com/googollee/go-socket.io/trim_writer.go @@ -0,0 +1,45 @@ +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/gorilla/context/.travis.yml b/vendor/github.com/gorilla/context/.travis.yml new file mode 100644 index 0000000000..6f440f1e42 --- /dev/null +++ b/vendor/github.com/gorilla/context/.travis.yml @@ -0,0 +1,19 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.3 + - go: 1.4 + - go: 1.5 + - go: 1.6 + - go: 1.7 + - go: tip + allow_failures: + - go: tip + +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d .) + - go vet $(go list ./... | grep -v /vendor/) + - go test -v -race ./... diff --git a/vendor/github.com/gorilla/context/LICENSE b/vendor/github.com/gorilla/context/LICENSE new file mode 100644 index 0000000000..0e5fb87280 --- /dev/null +++ b/vendor/github.com/gorilla/context/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/context/README.md b/vendor/github.com/gorilla/context/README.md new file mode 100644 index 0000000000..08f86693bc --- /dev/null +++ b/vendor/github.com/gorilla/context/README.md @@ -0,0 +1,10 @@ +context +======= +[![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context) + +gorilla/context is a general purpose registry for global request variables. + +> Note: gorilla/context, having been born well before `context.Context` existed, does not play well +> with the shallow copying of the request that [`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) (added to net/http Go 1.7 onwards) performs. You should either use *just* gorilla/context, or moving forward, the new `http.Request.Context()`. + +Read the full documentation here: http://www.gorillatoolkit.org/pkg/context diff --git a/vendor/github.com/gorilla/context/context.go b/vendor/github.com/gorilla/context/context.go new file mode 100644 index 0000000000..81cb128b19 --- /dev/null +++ b/vendor/github.com/gorilla/context/context.go @@ -0,0 +1,143 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package context + +import ( + "net/http" + "sync" + "time" +) + +var ( + mutex sync.RWMutex + data = make(map[*http.Request]map[interface{}]interface{}) + datat = make(map[*http.Request]int64) +) + +// Set stores a value for a given key in a given request. +func Set(r *http.Request, key, val interface{}) { + mutex.Lock() + if data[r] == nil { + data[r] = make(map[interface{}]interface{}) + datat[r] = time.Now().Unix() + } + data[r][key] = val + mutex.Unlock() +} + +// Get returns a value stored for a given key in a given request. +func Get(r *http.Request, key interface{}) interface{} { + mutex.RLock() + if ctx := data[r]; ctx != nil { + value := ctx[key] + mutex.RUnlock() + return value + } + mutex.RUnlock() + return nil +} + +// GetOk returns stored value and presence state like multi-value return of map access. +func GetOk(r *http.Request, key interface{}) (interface{}, bool) { + mutex.RLock() + if _, ok := data[r]; ok { + value, ok := data[r][key] + mutex.RUnlock() + return value, ok + } + mutex.RUnlock() + return nil, false +} + +// GetAll returns all stored values for the request as a map. Nil is returned for invalid requests. +func GetAll(r *http.Request) map[interface{}]interface{} { + mutex.RLock() + if context, ok := data[r]; ok { + result := make(map[interface{}]interface{}, len(context)) + for k, v := range context { + result[k] = v + } + mutex.RUnlock() + return result + } + mutex.RUnlock() + return nil +} + +// GetAllOk returns all stored values for the request as a map and a boolean value that indicates if +// the request was registered. +func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) { + mutex.RLock() + context, ok := data[r] + result := make(map[interface{}]interface{}, len(context)) + for k, v := range context { + result[k] = v + } + mutex.RUnlock() + return result, ok +} + +// Delete removes a value stored for a given key in a given request. +func Delete(r *http.Request, key interface{}) { + mutex.Lock() + if data[r] != nil { + delete(data[r], key) + } + mutex.Unlock() +} + +// Clear removes all values stored for a given request. +// +// This is usually called by a handler wrapper to clean up request +// variables at the end of a request lifetime. See ClearHandler(). +func Clear(r *http.Request) { + mutex.Lock() + clear(r) + mutex.Unlock() +} + +// clear is Clear without the lock. +func clear(r *http.Request) { + delete(data, r) + delete(datat, r) +} + +// Purge removes request data stored for longer than maxAge, in seconds. +// It returns the amount of requests removed. +// +// If maxAge <= 0, all request data is removed. +// +// This is only used for sanity check: in case context cleaning was not +// properly set some request data can be kept forever, consuming an increasing +// amount of memory. In case this is detected, Purge() must be called +// periodically until the problem is fixed. +func Purge(maxAge int) int { + mutex.Lock() + count := 0 + if maxAge <= 0 { + count = len(data) + data = make(map[*http.Request]map[interface{}]interface{}) + datat = make(map[*http.Request]int64) + } else { + min := time.Now().Unix() - int64(maxAge) + for r := range data { + if datat[r] < min { + clear(r) + count++ + } + } + } + mutex.Unlock() + return count +} + +// ClearHandler wraps an http.Handler and clears request values at the end +// of a request lifetime. +func ClearHandler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer Clear(r) + h.ServeHTTP(w, r) + }) +} diff --git a/vendor/github.com/gorilla/context/doc.go b/vendor/github.com/gorilla/context/doc.go new file mode 100644 index 0000000000..448d1bfcac --- /dev/null +++ b/vendor/github.com/gorilla/context/doc.go @@ -0,0 +1,88 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package context stores values shared during a request lifetime. + +Note: gorilla/context, having been born well before `context.Context` existed, +does not play well > with the shallow copying of the request that +[`http.Request.WithContext`](https://golang.org/pkg/net/http/#Request.WithContext) +(added to net/http Go 1.7 onwards) performs. You should either use *just* +gorilla/context, or moving forward, the new `http.Request.Context()`. + +For example, a router can set variables extracted from the URL and later +application handlers can access those values, or it can be used to store +sessions values to be saved at the end of a request. There are several +others common uses. + +The idea was posted by Brad Fitzpatrick to the go-nuts mailing list: + + http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53 + +Here's the basic usage: first define the keys that you will need. The key +type is interface{} so a key can be of any type that supports equality. +Here we define a key using a custom int type to avoid name collisions: + + package foo + + import ( + "github.com/gorilla/context" + ) + + type key int + + const MyKey key = 0 + +Then set a variable. Variables are bound to an http.Request object, so you +need a request instance to set a value: + + context.Set(r, MyKey, "bar") + +The application can later access the variable using the same key you provided: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + // val is "bar". + val := context.Get(r, foo.MyKey) + + // returns ("bar", true) + val, ok := context.GetOk(r, foo.MyKey) + // ... + } + +And that's all about the basic usage. We discuss some other ideas below. + +Any type can be stored in the context. To enforce a given type, make the key +private and wrap Get() and Set() to accept and return values of a specific +type: + + type key int + + const mykey key = 0 + + // GetMyKey returns a value for this package from the request values. + func GetMyKey(r *http.Request) SomeType { + if rv := context.Get(r, mykey); rv != nil { + return rv.(SomeType) + } + return nil + } + + // SetMyKey sets a value for this package in the request values. + func SetMyKey(r *http.Request, val SomeType) { + context.Set(r, mykey, val) + } + +Variables must be cleared at the end of a request, to remove all values +that were stored. This can be done in an http.Handler, after a request was +served. Just call Clear() passing the request: + + context.Clear(r) + +...or use ClearHandler(), which conveniently wraps an http.Handler to clear +variables at the end of a request lifetime. + +The Routers from the packages gorilla/mux and gorilla/pat call Clear() +so if you are using either of them you don't need to clear the context manually. +*/ +package context diff --git a/vendor/github.com/gorilla/mux/.travis.yml b/vendor/github.com/gorilla/mux/.travis.yml new file mode 100644 index 0000000000..ad0935dbd3 --- /dev/null +++ b/vendor/github.com/gorilla/mux/.travis.yml @@ -0,0 +1,23 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.5.x + - go: 1.6.x + - go: 1.7.x + - go: 1.8.x + - go: 1.9.x + - go: 1.10.x + - go: tip + allow_failures: + - go: tip + +install: + - # Skip + +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d .) + - go tool vet . + - go test -v -race ./... diff --git a/vendor/github.com/gorilla/mux/ISSUE_TEMPLATE.md b/vendor/github.com/gorilla/mux/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..232be82e47 --- /dev/null +++ b/vendor/github.com/gorilla/mux/ISSUE_TEMPLATE.md @@ -0,0 +1,11 @@ +**What version of Go are you running?** (Paste the output of `go version`) + + +**What version of gorilla/mux are you at?** (Paste the output of `git rev-parse HEAD` inside `$GOPATH/src/github.com/gorilla/mux`) + + +**Describe your problem** (and what you have tried so far) + + +**Paste a minimal, runnable, reproduction of your issue below** (use backticks to format it) + diff --git a/vendor/github.com/gorilla/mux/LICENSE b/vendor/github.com/gorilla/mux/LICENSE new file mode 100644 index 0000000000..0e5fb87280 --- /dev/null +++ b/vendor/github.com/gorilla/mux/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/mux/README.md b/vendor/github.com/gorilla/mux/README.md new file mode 100644 index 0000000000..e424397ac4 --- /dev/null +++ b/vendor/github.com/gorilla/mux/README.md @@ -0,0 +1,649 @@ +# gorilla/mux + +[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) +[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) + +![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png) + +http://www.gorillatoolkit.org/pkg/mux + +Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to +their respective handler. + +The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: + +* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. +* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. +* URL hosts, paths and query values can have variables with an optional regular expression. +* Registered URLs can be built, or "reversed", which helps maintaining references to resources. +* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. + +--- + +* [Install](#install) +* [Examples](#examples) +* [Matching Routes](#matching-routes) +* [Static Files](#static-files) +* [Registered URLs](#registered-urls) +* [Walking Routes](#walking-routes) +* [Graceful Shutdown](#graceful-shutdown) +* [Middleware](#middleware) +* [Testing Handlers](#testing-handlers) +* [Full Example](#full-example) + +--- + +## Install + +With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain: + +```sh +go get -u github.com/gorilla/mux +``` + +## Examples + +Let's start registering a couple of URL paths and handlers: + +```go +func main() { + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) +} +``` + +Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters. + +Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example: + +```go +r := mux.NewRouter() +r.HandleFunc("/products/{key}", ProductHandler) +r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +``` + +The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`: + +```go +func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Category: %v\n", vars["category"]) +} +``` + +And this is all you need to know about the basic usage. More advanced options are explained below. + +### Matching Routes + +Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: + +```go +r := mux.NewRouter() +// Only matches if domain is "www.example.com". +r.Host("www.example.com") +// Matches a dynamic subdomain. +r.Host("{subdomain:[a-z]+}.domain.com") +``` + +There are several other matchers that can be added. To match path prefixes: + +```go +r.PathPrefix("/products/") +``` + +...or HTTP methods: + +```go +r.Methods("GET", "POST") +``` + +...or URL schemes: + +```go +r.Schemes("https") +``` + +...or header values: + +```go +r.Headers("X-Requested-With", "XMLHttpRequest") +``` + +...or query values: + +```go +r.Queries("key", "value") +``` + +...or to use a custom matcher function: + +```go +r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 +}) +``` + +...and finally, it is possible to combine several matchers in a single route: + +```go +r.HandleFunc("/products", ProductsHandler). + Host("www.example.com"). + Methods("GET"). + Schemes("http") +``` + +Routes are tested in the order they were added to the router. If two routes match, the first one wins: + +```go +r := mux.NewRouter() +r.HandleFunc("/specific", specificHandler) +r.PathPrefix("/").Handler(catchAllHandler) +``` + +Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting". + +For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it: + +```go +r := mux.NewRouter() +s := r.Host("www.example.com").Subrouter() +``` + +Then register routes in the subrouter: + +```go +s.HandleFunc("/products/", ProductsHandler) +s.HandleFunc("/products/{key}", ProductHandler) +s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +``` + +The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. + +Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter. + +There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths: + +```go +r := mux.NewRouter() +s := r.PathPrefix("/products").Subrouter() +// "/products/" +s.HandleFunc("/", ProductsHandler) +// "/products/{key}/" +s.HandleFunc("/{key}/", ProductHandler) +// "/products/{key}/details" +s.HandleFunc("/{key}/details", ProductDetailsHandler) +``` + + +### Static Files + +Note that the path provided to `PathPrefix()` represents a "wildcard": calling +`PathPrefix("/static/").Handler(...)` means that the handler will be passed any +request that matches "/static/\*". This makes it easy to serve static files with mux: + +```go +func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/ + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} +``` + +### Registered URLs + +Now let's see how to build registered URLs. + +Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: + +```go +r := mux.NewRouter() +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") +``` + +To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do: + +```go +url, err := r.Get("article").URL("category", "technology", "id", "42") +``` + +...and the result will be a `url.URL` with the following path: + +``` +"/articles/technology/42" +``` + +This also works for host and query value variables: + +```go +r := mux.NewRouter() +r.Host("{subdomain}.domain.com"). + Path("/articles/{category}/{id:[0-9]+}"). + Queries("filter", "{filter}"). + HandlerFunc(ArticleHandler). + Name("article") + +// url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42", + "filter", "gorilla") +``` + +All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. + +Regex support also exists for matching Headers within a route. For example, we could do: + +```go +r.HeadersRegexp("Content-Type", "application/(text|json)") +``` + +...and the route will match both requests with a Content-Type of `application/json` as well as `application/text` + +There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: + +```go +// "http://news.domain.com/" +host, err := r.Get("article").URLHost("subdomain", "news") + +// "/articles/technology/42" +path, err := r.Get("article").URLPath("category", "technology", "id", "42") +``` + +And if you use subrouters, host and path defined separately can be built as well: + +```go +r := mux.NewRouter() +s := r.Host("{subdomain}.domain.com").Subrouter() +s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + +// "http://news.domain.com/articles/technology/42" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") +``` + +### Walking Routes + +The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example, +the following prints all of the registered routes: + +```go +package main + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gorilla/mux" +) + +func handler(w http.ResponseWriter, r *http.Request) { + return +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/", handler) + r.HandleFunc("/products", handler).Methods("POST") + r.HandleFunc("/articles", handler).Methods("GET") + r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT") + r.HandleFunc("/authors", handler).Queries("surname", "{surname}") + err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err == nil { + fmt.Println("ROUTE:", pathTemplate) + } + pathRegexp, err := route.GetPathRegexp() + if err == nil { + fmt.Println("Path regexp:", pathRegexp) + } + queriesTemplates, err := route.GetQueriesTemplates() + if err == nil { + fmt.Println("Queries templates:", strings.Join(queriesTemplates, ",")) + } + queriesRegexps, err := route.GetQueriesRegexp() + if err == nil { + fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ",")) + } + methods, err := route.GetMethods() + if err == nil { + fmt.Println("Methods:", strings.Join(methods, ",")) + } + fmt.Println() + return nil + }) + + if err != nil { + fmt.Println(err) + } + + http.Handle("/", r) +} +``` + +### Graceful Shutdown + +Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`: + +```go +package main + +import ( + "context" + "flag" + "log" + "net/http" + "os" + "os/signal" + "time" + + "github.com/gorilla/mux" +) + +func main() { + var wait time.Duration + flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m") + flag.Parse() + + r := mux.NewRouter() + // Add your routes as needed + + srv := &http.Server{ + Addr: "0.0.0.0:8080", + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: r, // Pass our instance of gorilla/mux in. + } + + // Run our server in a goroutine so that it doesn't block. + go func() { + if err := srv.ListenAndServe(); err != nil { + log.Println(err) + } + }() + + c := make(chan os.Signal, 1) + // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) + // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. + signal.Notify(c, os.Interrupt) + + // Block until we receive our signal. + <-c + + // Create a deadline to wait for. + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + // Doesn't block if no connections, but will otherwise wait + // until the timeout deadline. + srv.Shutdown(ctx) + // Optionally, you could run srv.Shutdown in a goroutine and block on + // <-ctx.Done() if your application should wait for other services + // to finalize based on context cancellation. + log.Println("shutting down") + os.Exit(0) +} +``` + +### Middleware + +Mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed in the order they are added if a match is found, including its subrouters. +Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or `ResponseWriter` hijacking. + +Mux middlewares are defined using the de facto standard type: + +```go +type MiddlewareFunc func(http.Handler) http.Handler +``` + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc. This takes advantage of closures being able access variables from the context where they are created, while retaining the signature enforced by the receivers. + +A very basic middleware which logs the URI of the request being handled could be written as: + +```go +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) +} +``` + +Middlewares can be added to a router using `Router.Use()`: + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) +r.Use(loggingMiddleware) +``` + +A more complex authentication middleware, which maps session token to users, could be written as: + +```go +// Define our struct +type authenticationMiddleware struct { + tokenUsers map[string]string +} + +// Initialize it somewhere +func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" +} + +// Middleware function, which will be called for each request +func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + // Pass down the request to the next middleware (or final handler) + next.ServeHTTP(w, r) + } else { + // Write an error and stop the handler chain + http.Error(w, "Forbidden", http.StatusForbidden) + } + }) +} +``` + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) + +amw := authenticationMiddleware{} +amw.Populate() + +r.Use(amw.Middleware) +``` + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it. + +### Testing Handlers + +Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_. + +First, our simple HTTP handler: + +```go +// endpoints.go +package main + +func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { + // A very simple health check. + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + + // In the future we could report back on the status of our DB, or our cache + // (e.g. Redis) by performing a simple PING, and include them in the response. + io.WriteString(w, `{"alive": true}`) +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/health", HealthCheckHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test code: + +```go +// endpoints_test.go +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthCheckHandler(t *testing.T) { + // Create a request to pass to our handler. We don't have any query parameters for now, so we'll + // pass 'nil' as the third parameter. + req, err := http.NewRequest("GET", "/health", nil) + if err != nil { + t.Fatal(err) + } + + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + rr := httptest.NewRecorder() + handler := http.HandlerFunc(HealthCheckHandler) + + // Our handlers satisfy http.Handler, so we can call their ServeHTTP method + // directly and pass in our Request and ResponseRecorder. + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := `{"alive": true}` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", + rr.Body.String(), expected) + } +} +``` + +In the case that our routes have [variables](#examples), we can pass those in the request. We could write +[table-driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) to test multiple +possible route variables as needed. + +```go +// endpoints.go +func main() { + r := mux.NewRouter() + // A route with a route variable: + r.HandleFunc("/metrics/{type}", MetricsHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test file, with a table-driven test of `routeVariables`: + +```go +// endpoints_test.go +func TestMetricsHandler(t *testing.T) { + tt := []struct{ + routeVariable string + shouldPass bool + }{ + {"goroutines", true}, + {"heap", true}, + {"counters", true}, + {"queries", true}, + {"adhadaeqm3k", false}, + } + + for _, tc := range tt { + path := fmt.Sprintf("/metrics/%s", tc.routeVariable) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + + // Need to create a router that we can pass the request through so that the vars will be added to the context + router := mux.NewRouter() + router.HandleFunc("/metrics/{type}", MetricsHandler) + router.ServeHTTP(rr, req) + + // In this case, our MetricsHandler returns a non-200 response + // for a route variable it doesn't know about. + if rr.Code == http.StatusOK && !tc.shouldPass { + t.Errorf("handler should have failed on routeVariable %s: got %v want %v", + tc.routeVariable, rr.Code, http.StatusOK) + } + } +} +``` + +## Full Example + +Here's a complete, runnable example of a small `mux` based server: + +```go +package main + +import ( + "net/http" + "log" + "github.com/gorilla/mux" +) + +func YourHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Gorilla!\n")) +} + +func main() { + r := mux.NewRouter() + // Routes consist of a path and a handler function. + r.HandleFunc("/", YourHandler) + + // Bind to a port and pass our router in + log.Fatal(http.ListenAndServe(":8000", r)) +} +``` + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/mux/context_gorilla.go b/vendor/github.com/gorilla/mux/context_gorilla.go new file mode 100644 index 0000000000..d7adaa8fad --- /dev/null +++ b/vendor/github.com/gorilla/mux/context_gorilla.go @@ -0,0 +1,26 @@ +// +build !go1.7 + +package mux + +import ( + "net/http" + + "github.com/gorilla/context" +) + +func contextGet(r *http.Request, key interface{}) interface{} { + return context.Get(r, key) +} + +func contextSet(r *http.Request, key, val interface{}) *http.Request { + if val == nil { + return r + } + + context.Set(r, key, val) + return r +} + +func contextClear(r *http.Request) { + context.Clear(r) +} diff --git a/vendor/github.com/gorilla/mux/context_native.go b/vendor/github.com/gorilla/mux/context_native.go new file mode 100644 index 0000000000..209cbea7d6 --- /dev/null +++ b/vendor/github.com/gorilla/mux/context_native.go @@ -0,0 +1,24 @@ +// +build go1.7 + +package mux + +import ( + "context" + "net/http" +) + +func contextGet(r *http.Request, key interface{}) interface{} { + return r.Context().Value(key) +} + +func contextSet(r *http.Request, key, val interface{}) *http.Request { + if val == nil { + return r + } + + return r.WithContext(context.WithValue(r.Context(), key, val)) +} + +func contextClear(r *http.Request) { + return +} diff --git a/vendor/github.com/gorilla/mux/doc.go b/vendor/github.com/gorilla/mux/doc.go new file mode 100644 index 0000000000..38957deead --- /dev/null +++ b/vendor/github.com/gorilla/mux/doc.go @@ -0,0 +1,306 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package mux implements a request router and dispatcher. + +The name mux stands for "HTTP request multiplexer". Like the standard +http.ServeMux, mux.Router matches incoming requests against a list of +registered routes and calls a handler for the route that matches the URL +or other conditions. The main features are: + + * Requests can be matched based on URL host, path, path prefix, schemes, + header and query values, HTTP methods or using custom matchers. + * URL hosts, paths and query values can have variables with an optional + regular expression. + * Registered URLs can be built, or "reversed", which helps maintaining + references to resources. + * Routes can be used as subrouters: nested routes are only tested if the + parent route matches. This is useful to define groups of routes that + share common conditions like a host, a path prefix or other repeated + attributes. As a bonus, this optimizes request matching. + * It implements the http.Handler interface so it is compatible with the + standard http.ServeMux. + +Let's start registering a couple of URL paths and handlers: + + func main() { + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) + } + +Here we register three routes mapping URL paths to handlers. This is +equivalent to how http.HandleFunc() works: if an incoming request URL matches +one of the paths, the corresponding handler is called passing +(http.ResponseWriter, *http.Request) as parameters. + +Paths can have variables. They are defined using the format {name} or +{name:pattern}. If a regular expression pattern is not defined, the matched +variable will be anything until the next slash. For example: + + r := mux.NewRouter() + r.HandleFunc("/products/{key}", ProductHandler) + r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) + +Groups can be used inside patterns, as long as they are non-capturing (?:re). For example: + + r.HandleFunc("/articles/{category}/{sort:(?:asc|desc|new)}", ArticlesCategoryHandler) + +The names are used to create a map of route variables which can be retrieved +calling mux.Vars(): + + vars := mux.Vars(request) + category := vars["category"] + +Note that if any capturing groups are present, mux will panic() during parsing. To prevent +this, convert any capturing groups to non-capturing, e.g. change "/{sort:(asc|desc)}" to +"/{sort:(?:asc|desc)}". This is a change from prior versions which behaved unpredictably +when capturing groups were present. + +And this is all you need to know about the basic usage. More advanced options +are explained below. + +Routes can also be restricted to a domain or subdomain. Just define a host +pattern to be matched. They can also have variables: + + r := mux.NewRouter() + // Only matches if domain is "www.example.com". + r.Host("www.example.com") + // Matches a dynamic subdomain. + r.Host("{subdomain:[a-z]+}.domain.com") + +There are several other matchers that can be added. To match path prefixes: + + r.PathPrefix("/products/") + +...or HTTP methods: + + r.Methods("GET", "POST") + +...or URL schemes: + + r.Schemes("https") + +...or header values: + + r.Headers("X-Requested-With", "XMLHttpRequest") + +...or query values: + + r.Queries("key", "value") + +...or to use a custom matcher function: + + r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 + }) + +...and finally, it is possible to combine several matchers in a single route: + + r.HandleFunc("/products", ProductsHandler). + Host("www.example.com"). + Methods("GET"). + Schemes("http") + +Setting the same matching conditions again and again can be boring, so we have +a way to group several routes that share the same requirements. +We call it "subrouting". + +For example, let's say we have several URLs that should only match when the +host is "www.example.com". Create a route for that host and get a "subrouter" +from it: + + r := mux.NewRouter() + s := r.Host("www.example.com").Subrouter() + +Then register routes in the subrouter: + + s.HandleFunc("/products/", ProductsHandler) + s.HandleFunc("/products/{key}", ProductHandler) + s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) + +The three URL paths we registered above will only be tested if the domain is +"www.example.com", because the subrouter is tested first. This is not +only convenient, but also optimizes request matching. You can create +subrouters combining any attribute matchers accepted by a route. + +Subrouters can be used to create domain or path "namespaces": you define +subrouters in a central place and then parts of the app can register its +paths relatively to a given subrouter. + +There's one more thing about subroutes. When a subrouter has a path prefix, +the inner routes use it as base for their paths: + + r := mux.NewRouter() + s := r.PathPrefix("/products").Subrouter() + // "/products/" + s.HandleFunc("/", ProductsHandler) + // "/products/{key}/" + s.HandleFunc("/{key}/", ProductHandler) + // "/products/{key}/details" + s.HandleFunc("/{key}/details", ProductDetailsHandler) + +Note that the path provided to PathPrefix() represents a "wildcard": calling +PathPrefix("/static/").Handler(...) means that the handler will be passed any +request that matches "/static/*". This makes it easy to serve static files with mux: + + func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/ + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) + } + +Now let's see how to build registered URLs. + +Routes can be named. All routes that define a name can have their URLs built, +or "reversed". We define a name calling Name() on a route. For example: + + r := mux.NewRouter() + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") + +To build a URL, get the route and call the URL() method, passing a sequence of +key/value pairs for the route variables. For the previous route, we would do: + + url, err := r.Get("article").URL("category", "technology", "id", "42") + +...and the result will be a url.URL with the following path: + + "/articles/technology/42" + +This also works for host and query value variables: + + r := mux.NewRouter() + r.Host("{subdomain}.domain.com"). + Path("/articles/{category}/{id:[0-9]+}"). + Queries("filter", "{filter}"). + HandlerFunc(ArticleHandler). + Name("article") + + // url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42", + "filter", "gorilla") + +All variables defined in the route are required, and their values must +conform to the corresponding patterns. These requirements guarantee that a +generated URL will always match a registered route -- the only exception is +for explicitly defined "build-only" routes which never match. + +Regex support also exists for matching Headers within a route. For example, we could do: + + r.HeadersRegexp("Content-Type", "application/(text|json)") + +...and the route will match both requests with a Content-Type of `application/json` as well as +`application/text` + +There's also a way to build only the URL host or path for a route: +use the methods URLHost() or URLPath() instead. For the previous route, +we would do: + + // "http://news.domain.com/" + host, err := r.Get("article").URLHost("subdomain", "news") + + // "/articles/technology/42" + path, err := r.Get("article").URLPath("category", "technology", "id", "42") + +And if you use subrouters, host and path defined separately can be built +as well: + + r := mux.NewRouter() + s := r.Host("{subdomain}.domain.com").Subrouter() + s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + + // "http://news.domain.com/articles/technology/42" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") + +Mux supports the addition of middlewares to a Router, which are executed in the order they are added if a match is found, including its subrouters. Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or ResponseWriter hijacking. + + type MiddlewareFunc func(http.Handler) http.Handler + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created). + +A very basic middleware which logs the URI of the request being handled could be written as: + + func simpleMw(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) + } + +Middlewares can be added to a router using `Router.Use()`: + + r := mux.NewRouter() + r.HandleFunc("/", handler) + r.Use(simpleMw) + +A more complex authentication middleware, which maps session token to users, could be written as: + + // Define our struct + type authenticationMiddleware struct { + tokenUsers map[string]string + } + + // Initialize it somewhere + func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" + } + + // Middleware function, which will be called for each request + func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + next.ServeHTTP(w, r) + } else { + http.Error(w, "Forbidden", http.StatusForbidden) + } + }) + } + + r := mux.NewRouter() + r.HandleFunc("/", handler) + + amw := authenticationMiddleware{} + amw.Populate() + + r.Use(amw.Middleware) + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. + +*/ +package mux diff --git a/vendor/github.com/gorilla/mux/middleware.go b/vendor/github.com/gorilla/mux/middleware.go new file mode 100644 index 0000000000..ceb812cee2 --- /dev/null +++ b/vendor/github.com/gorilla/mux/middleware.go @@ -0,0 +1,72 @@ +package mux + +import ( + "net/http" + "strings" +) + +// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler. +// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed +// to it, and then calls the handler passed as parameter to the MiddlewareFunc. +type MiddlewareFunc func(http.Handler) http.Handler + +// middleware interface is anything which implements a MiddlewareFunc named Middleware. +type middleware interface { + Middleware(handler http.Handler) http.Handler +} + +// Middleware allows MiddlewareFunc to implement the middleware interface. +func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler { + return mw(handler) +} + +// Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) Use(mwf ...MiddlewareFunc) { + for _, fn := range mwf { + r.middlewares = append(r.middlewares, fn) + } +} + +// useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) useInterface(mw middleware) { + r.middlewares = append(r.middlewares, mw) +} + +// CORSMethodMiddleware sets the Access-Control-Allow-Methods response header +// on a request, by matching routes based only on paths. It also handles +// OPTIONS requests, by settings Access-Control-Allow-Methods, and then +// returning without calling the next http handler. +func CORSMethodMiddleware(r *Router) MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var allMethods []string + + err := r.Walk(func(route *Route, _ *Router, _ []*Route) error { + for _, m := range route.matchers { + if _, ok := m.(*routeRegexp); ok { + if m.Match(req, &RouteMatch{}) { + methods, err := route.GetMethods() + if err != nil { + return err + } + + allMethods = append(allMethods, methods...) + } + break + } + } + return nil + }) + + if err == nil { + w.Header().Set("Access-Control-Allow-Methods", strings.Join(append(allMethods, "OPTIONS"), ",")) + + if req.Method == "OPTIONS" { + return + } + } + + next.ServeHTTP(w, req) + }) + } +} diff --git a/vendor/github.com/gorilla/mux/mux.go b/vendor/github.com/gorilla/mux/mux.go new file mode 100644 index 0000000000..4bbafa51da --- /dev/null +++ b/vendor/github.com/gorilla/mux/mux.go @@ -0,0 +1,588 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "errors" + "fmt" + "net/http" + "path" + "regexp" +) + +var ( + // ErrMethodMismatch is returned when the method in the request does not match + // the method defined against the route. + ErrMethodMismatch = errors.New("method is not allowed") + // ErrNotFound is returned when no route match is found. + ErrNotFound = errors.New("no matching route was found") +) + +// NewRouter returns a new router instance. +func NewRouter() *Router { + return &Router{namedRoutes: make(map[string]*Route), KeepContext: false} +} + +// Router registers routes to be matched and dispatches a handler. +// +// It implements the http.Handler interface, so it can be registered to serve +// requests: +// +// var router = mux.NewRouter() +// +// func main() { +// http.Handle("/", router) +// } +// +// Or, for Google App Engine, register it in a init() function: +// +// func init() { +// http.Handle("/", router) +// } +// +// This will send all incoming requests to the router. +type Router struct { + // Configurable Handler to be used when no route matches. + NotFoundHandler http.Handler + + // Configurable Handler to be used when the request method does not match the route. + MethodNotAllowedHandler http.Handler + + // Parent route, if this is a subrouter. + parent parentRoute + // Routes to be matched, in order. + routes []*Route + // Routes by name for URL building. + namedRoutes map[string]*Route + // See Router.StrictSlash(). This defines the flag for new routes. + strictSlash bool + // See Router.SkipClean(). This defines the flag for new routes. + skipClean bool + // If true, do not clear the request context after handling the request. + // This has no effect when go1.7+ is used, since the context is stored + // on the request itself. + KeepContext bool + // see Router.UseEncodedPath(). This defines a flag for all routes. + useEncodedPath bool + // Slice of middlewares to be called after a match is found + middlewares []middleware +} + +// Match attempts to match the given request against the router's registered routes. +// +// If the request matches a route of this router or one of its subrouters the Route, +// Handler, and Vars fields of the the match argument are filled and this function +// returns true. +// +// If the request does not match any of this router's or its subrouters' routes +// then this function returns false. If available, a reason for the match failure +// will be filled in the match argument's MatchErr field. If the match failure type +// (eg: not found) has a registered handler, the handler is assigned to the Handler +// field of the match argument. +func (r *Router) Match(req *http.Request, match *RouteMatch) bool { + for _, route := range r.routes { + if route.Match(req, match) { + // Build middleware chain if no error was found + if match.MatchErr == nil { + for i := len(r.middlewares) - 1; i >= 0; i-- { + match.Handler = r.middlewares[i].Middleware(match.Handler) + } + } + return true + } + } + + if match.MatchErr == ErrMethodMismatch { + if r.MethodNotAllowedHandler != nil { + match.Handler = r.MethodNotAllowedHandler + return true + } + + return false + } + + // Closest match for a router (includes sub-routers) + if r.NotFoundHandler != nil { + match.Handler = r.NotFoundHandler + match.MatchErr = ErrNotFound + return true + } + + match.MatchErr = ErrNotFound + return false +} + +// ServeHTTP dispatches the handler registered in the matched route. +// +// When there is a match, the route variables can be retrieved calling +// mux.Vars(request). +func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if !r.skipClean { + path := req.URL.Path + if r.useEncodedPath { + path = req.URL.EscapedPath() + } + // Clean path to canonical form and redirect. + if p := cleanPath(path); p != path { + + // Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query. + // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: + // http://code.google.com/p/go/issues/detail?id=5252 + url := *req.URL + url.Path = p + p = url.String() + + w.Header().Set("Location", p) + w.WriteHeader(http.StatusMovedPermanently) + return + } + } + var match RouteMatch + var handler http.Handler + if r.Match(req, &match) { + handler = match.Handler + req = setVars(req, match.Vars) + req = setCurrentRoute(req, match.Route) + } + + if handler == nil && match.MatchErr == ErrMethodMismatch { + handler = methodNotAllowedHandler() + } + + if handler == nil { + handler = http.NotFoundHandler() + } + + if !r.KeepContext { + defer contextClear(req) + } + + handler.ServeHTTP(w, req) +} + +// Get returns a route registered with the given name. +func (r *Router) Get(name string) *Route { + return r.getNamedRoutes()[name] +} + +// GetRoute returns a route registered with the given name. This method +// was renamed to Get() and remains here for backwards compatibility. +func (r *Router) GetRoute(name string) *Route { + return r.getNamedRoutes()[name] +} + +// StrictSlash defines the trailing slash behavior for new routes. The initial +// value is false. +// +// When true, if the route path is "/path/", accessing "/path" will perform a redirect +// to the former and vice versa. In other words, your application will always +// see the path as specified in the route. +// +// When false, if the route path is "/path", accessing "/path/" will not match +// this route and vice versa. +// +// The re-direct is a HTTP 301 (Moved Permanently). Note that when this is set for +// routes with a non-idempotent method (e.g. POST, PUT), the subsequent re-directed +// request will be made as a GET by most clients. Use middleware or client settings +// to modify this behaviour as needed. +// +// Special case: when a route sets a path prefix using the PathPrefix() method, +// strict slash is ignored for that route because the redirect behavior can't +// be determined from a prefix alone. However, any subrouters created from that +// route inherit the original StrictSlash setting. +func (r *Router) StrictSlash(value bool) *Router { + r.strictSlash = value + return r +} + +// SkipClean defines the path cleaning behaviour for new routes. The initial +// value is false. Users should be careful about which routes are not cleaned +// +// When true, if the route path is "/path//to", it will remain with the double +// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/ +// +// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will +// become /fetch/http/xkcd.com/534 +func (r *Router) SkipClean(value bool) *Router { + r.skipClean = value + return r +} + +// UseEncodedPath tells the router to match the encoded original path +// to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to". +// +// If not called, the router will match the unencoded path to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to" +func (r *Router) UseEncodedPath() *Router { + r.useEncodedPath = true + return r +} + +// ---------------------------------------------------------------------------- +// parentRoute +// ---------------------------------------------------------------------------- + +func (r *Router) getBuildScheme() string { + if r.parent != nil { + return r.parent.getBuildScheme() + } + return "" +} + +// getNamedRoutes returns the map where named routes are registered. +func (r *Router) getNamedRoutes() map[string]*Route { + if r.namedRoutes == nil { + if r.parent != nil { + r.namedRoutes = r.parent.getNamedRoutes() + } else { + r.namedRoutes = make(map[string]*Route) + } + } + return r.namedRoutes +} + +// getRegexpGroup returns regexp definitions from the parent route, if any. +func (r *Router) getRegexpGroup() *routeRegexpGroup { + if r.parent != nil { + return r.parent.getRegexpGroup() + } + return nil +} + +func (r *Router) buildVars(m map[string]string) map[string]string { + if r.parent != nil { + m = r.parent.buildVars(m) + } + return m +} + +// ---------------------------------------------------------------------------- +// Route factories +// ---------------------------------------------------------------------------- + +// NewRoute registers an empty route. +func (r *Router) NewRoute() *Route { + route := &Route{parent: r, strictSlash: r.strictSlash, skipClean: r.skipClean, useEncodedPath: r.useEncodedPath} + r.routes = append(r.routes, route) + return route +} + +// Handle registers a new route with a matcher for the URL path. +// See Route.Path() and Route.Handler(). +func (r *Router) Handle(path string, handler http.Handler) *Route { + return r.NewRoute().Path(path).Handler(handler) +} + +// HandleFunc registers a new route with a matcher for the URL path. +// See Route.Path() and Route.HandlerFunc(). +func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, + *http.Request)) *Route { + return r.NewRoute().Path(path).HandlerFunc(f) +} + +// Headers registers a new route with a matcher for request header values. +// See Route.Headers(). +func (r *Router) Headers(pairs ...string) *Route { + return r.NewRoute().Headers(pairs...) +} + +// Host registers a new route with a matcher for the URL host. +// See Route.Host(). +func (r *Router) Host(tpl string) *Route { + return r.NewRoute().Host(tpl) +} + +// MatcherFunc registers a new route with a custom matcher function. +// See Route.MatcherFunc(). +func (r *Router) MatcherFunc(f MatcherFunc) *Route { + return r.NewRoute().MatcherFunc(f) +} + +// Methods registers a new route with a matcher for HTTP methods. +// See Route.Methods(). +func (r *Router) Methods(methods ...string) *Route { + return r.NewRoute().Methods(methods...) +} + +// Path registers a new route with a matcher for the URL path. +// See Route.Path(). +func (r *Router) Path(tpl string) *Route { + return r.NewRoute().Path(tpl) +} + +// PathPrefix registers a new route with a matcher for the URL path prefix. +// See Route.PathPrefix(). +func (r *Router) PathPrefix(tpl string) *Route { + return r.NewRoute().PathPrefix(tpl) +} + +// Queries registers a new route with a matcher for URL query values. +// See Route.Queries(). +func (r *Router) Queries(pairs ...string) *Route { + return r.NewRoute().Queries(pairs...) +} + +// Schemes registers a new route with a matcher for URL schemes. +// See Route.Schemes(). +func (r *Router) Schemes(schemes ...string) *Route { + return r.NewRoute().Schemes(schemes...) +} + +// BuildVarsFunc registers a new route with a custom function for modifying +// route variables before building a URL. +func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { + return r.NewRoute().BuildVarsFunc(f) +} + +// Walk walks the router and all its sub-routers, calling walkFn for each route +// in the tree. The routes are walked in the order they were added. Sub-routers +// are explored depth-first. +func (r *Router) Walk(walkFn WalkFunc) error { + return r.walk(walkFn, []*Route{}) +} + +// SkipRouter is used as a return value from WalkFuncs to indicate that the +// router that walk is about to descend down to should be skipped. +var SkipRouter = errors.New("skip this router") + +// WalkFunc is the type of the function called for each route visited by Walk. +// At every invocation, it is given the current route, and the current router, +// and a list of ancestor routes that lead to the current route. +type WalkFunc func(route *Route, router *Router, ancestors []*Route) error + +func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error { + for _, t := range r.routes { + err := walkFn(t, r, ancestors) + if err == SkipRouter { + continue + } + if err != nil { + return err + } + for _, sr := range t.matchers { + if h, ok := sr.(*Router); ok { + ancestors = append(ancestors, t) + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + ancestors = ancestors[:len(ancestors)-1] + } + } + if h, ok := t.handler.(*Router); ok { + ancestors = append(ancestors, t) + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + ancestors = ancestors[:len(ancestors)-1] + } + } + return nil +} + +// ---------------------------------------------------------------------------- +// Context +// ---------------------------------------------------------------------------- + +// RouteMatch stores information about a matched route. +type RouteMatch struct { + Route *Route + Handler http.Handler + Vars map[string]string + + // MatchErr is set to appropriate matching error + // It is set to ErrMethodMismatch if there is a mismatch in + // the request method and route method + MatchErr error +} + +type contextKey int + +const ( + varsKey contextKey = iota + routeKey +) + +// Vars returns the route variables for the current request, if any. +func Vars(r *http.Request) map[string]string { + if rv := contextGet(r, varsKey); rv != nil { + return rv.(map[string]string) + } + return nil +} + +// CurrentRoute returns the matched route for the current request, if any. +// This only works when called inside the handler of the matched route +// because the matched route is stored in the request context which is cleared +// after the handler returns, unless the KeepContext option is set on the +// Router. +func CurrentRoute(r *http.Request) *Route { + if rv := contextGet(r, routeKey); rv != nil { + return rv.(*Route) + } + return nil +} + +func setVars(r *http.Request, val interface{}) *http.Request { + return contextSet(r, varsKey, val) +} + +func setCurrentRoute(r *http.Request, val interface{}) *http.Request { + return contextSet(r, routeKey, val) +} + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +// cleanPath returns the canonical path for p, eliminating . and .. elements. +// Borrowed from the net/http package. +func cleanPath(p string) string { + if p == "" { + return "/" + } + if p[0] != '/' { + p = "/" + p + } + np := path.Clean(p) + // path.Clean removes trailing slash except for root; + // put the trailing slash back if necessary. + if p[len(p)-1] == '/' && np != "/" { + np += "/" + } + + return np +} + +// uniqueVars returns an error if two slices contain duplicated strings. +func uniqueVars(s1, s2 []string) error { + for _, v1 := range s1 { + for _, v2 := range s2 { + if v1 == v2 { + return fmt.Errorf("mux: duplicated route variable %q", v2) + } + } + } + return nil +} + +// checkPairs returns the count of strings passed in, and an error if +// the count is not an even number. +func checkPairs(pairs ...string) (int, error) { + length := len(pairs) + if length%2 != 0 { + return length, fmt.Errorf( + "mux: number of parameters must be multiple of 2, got %v", pairs) + } + return length, nil +} + +// mapFromPairsToString converts variadic string parameters to a +// string to string map. +func mapFromPairsToString(pairs ...string) (map[string]string, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]string, length/2) + for i := 0; i < length; i += 2 { + m[pairs[i]] = pairs[i+1] + } + return m, nil +} + +// mapFromPairsToRegex converts variadic string parameters to a +// string to regex map. +func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]*regexp.Regexp, length/2) + for i := 0; i < length; i += 2 { + regex, err := regexp.Compile(pairs[i+1]) + if err != nil { + return nil, err + } + m[pairs[i]] = regex + } + return m, nil +} + +// matchInArray returns true if the given string value is in the array. +func matchInArray(arr []string, value string) bool { + for _, v := range arr { + if v == value { + return true + } + } + return false +} + +// matchMapWithString returns true if the given key/value pairs exist in a given map. +func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != "" { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v == value { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} + +// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against +// the given regex +func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != nil { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v.MatchString(value) { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} + +// methodNotAllowed replies to the request with an HTTP status code 405. +func methodNotAllowed(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) +} + +// methodNotAllowedHandler returns a simple request handler +// that replies to each request with a status code 405. +func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) } diff --git a/vendor/github.com/gorilla/mux/regexp.go b/vendor/github.com/gorilla/mux/regexp.go new file mode 100644 index 0000000000..2b57e5627d --- /dev/null +++ b/vendor/github.com/gorilla/mux/regexp.go @@ -0,0 +1,332 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "bytes" + "fmt" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" +) + +type routeRegexpOptions struct { + strictSlash bool + useEncodedPath bool +} + +type regexpType int + +const ( + regexpTypePath regexpType = 0 + regexpTypeHost regexpType = 1 + regexpTypePrefix regexpType = 2 + regexpTypeQuery regexpType = 3 +) + +// newRouteRegexp parses a route template and returns a routeRegexp, +// used to match a host, a path or a query string. +// +// It will extract named variables, assemble a regexp to be matched, create +// a "reverse" template to build URLs and compile regexps to validate variable +// values used in URL building. +// +// Previously we accepted only Python-like identifiers for variable +// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that +// name and pattern can't be empty, and names can't contain a colon. +func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) { + // Check if it is well-formed. + idxs, errBraces := braceIndices(tpl) + if errBraces != nil { + return nil, errBraces + } + // Backup the original. + template := tpl + // Now let's parse it. + defaultPattern := "[^/]+" + if typ == regexpTypeQuery { + defaultPattern = ".*" + } else if typ == regexpTypeHost { + defaultPattern = "[^.]+" + } + // Only match strict slash if not matching + if typ != regexpTypePath { + options.strictSlash = false + } + // Set a flag for strictSlash. + endSlash := false + if options.strictSlash && strings.HasSuffix(tpl, "/") { + tpl = tpl[:len(tpl)-1] + endSlash = true + } + varsN := make([]string, len(idxs)/2) + varsR := make([]*regexp.Regexp, len(idxs)/2) + pattern := bytes.NewBufferString("") + pattern.WriteByte('^') + reverse := bytes.NewBufferString("") + var end int + var err error + for i := 0; i < len(idxs); i += 2 { + // Set all values we are interested in. + raw := tpl[end:idxs[i]] + end = idxs[i+1] + parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) + name := parts[0] + patt := defaultPattern + if len(parts) == 2 { + patt = parts[1] + } + // Name or pattern can't be empty. + if name == "" || patt == "" { + return nil, fmt.Errorf("mux: missing name or pattern in %q", + tpl[idxs[i]:end]) + } + // Build the regexp pattern. + fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt) + + // Build the reverse template. + fmt.Fprintf(reverse, "%s%%s", raw) + + // Append variable name and compiled pattern. + varsN[i/2] = name + varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) + if err != nil { + return nil, err + } + } + // Add the remaining. + raw := tpl[end:] + pattern.WriteString(regexp.QuoteMeta(raw)) + if options.strictSlash { + pattern.WriteString("[/]?") + } + if typ == regexpTypeQuery { + // Add the default pattern if the query value is empty + if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { + pattern.WriteString(defaultPattern) + } + } + if typ != regexpTypePrefix { + pattern.WriteByte('$') + } + reverse.WriteString(raw) + if endSlash { + reverse.WriteByte('/') + } + // Compile full regexp. + reg, errCompile := regexp.Compile(pattern.String()) + if errCompile != nil { + return nil, errCompile + } + + // Check for capturing groups which used to work in older versions + if reg.NumSubexp() != len(idxs)/2 { + panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) + + "Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)") + } + + // Done! + return &routeRegexp{ + template: template, + regexpType: typ, + options: options, + regexp: reg, + reverse: reverse.String(), + varsN: varsN, + varsR: varsR, + }, nil +} + +// routeRegexp stores a regexp to match a host or path and information to +// collect and validate route variables. +type routeRegexp struct { + // The unmodified template. + template string + // The type of match + regexpType regexpType + // Options for matching + options routeRegexpOptions + // Expanded regexp. + regexp *regexp.Regexp + // Reverse template. + reverse string + // Variable names. + varsN []string + // Variable regexps (validators). + varsR []*regexp.Regexp +} + +// Match matches the regexp against the URL host or path. +func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { + if r.regexpType != regexpTypeHost { + if r.regexpType == regexpTypeQuery { + return r.matchQueryString(req) + } + path := req.URL.Path + if r.options.useEncodedPath { + path = req.URL.EscapedPath() + } + return r.regexp.MatchString(path) + } + + return r.regexp.MatchString(getHost(req)) +} + +// url builds a URL part using the given values. +func (r *routeRegexp) url(values map[string]string) (string, error) { + urlValues := make([]interface{}, len(r.varsN)) + for k, v := range r.varsN { + value, ok := values[v] + if !ok { + return "", fmt.Errorf("mux: missing route variable %q", v) + } + if r.regexpType == regexpTypeQuery { + value = url.QueryEscape(value) + } + urlValues[k] = value + } + rv := fmt.Sprintf(r.reverse, urlValues...) + if !r.regexp.MatchString(rv) { + // The URL is checked against the full regexp, instead of checking + // individual variables. This is faster but to provide a good error + // message, we check individual regexps if the URL doesn't match. + for k, v := range r.varsN { + if !r.varsR[k].MatchString(values[v]) { + return "", fmt.Errorf( + "mux: variable %q doesn't match, expected %q", values[v], + r.varsR[k].String()) + } + } + } + return rv, nil +} + +// getURLQuery returns a single query parameter from a request URL. +// For a URL with foo=bar&baz=ding, we return only the relevant key +// value pair for the routeRegexp. +func (r *routeRegexp) getURLQuery(req *http.Request) string { + if r.regexpType != regexpTypeQuery { + return "" + } + templateKey := strings.SplitN(r.template, "=", 2)[0] + for key, vals := range req.URL.Query() { + if key == templateKey && len(vals) > 0 { + return key + "=" + vals[0] + } + } + return "" +} + +func (r *routeRegexp) matchQueryString(req *http.Request) bool { + return r.regexp.MatchString(r.getURLQuery(req)) +} + +// braceIndices returns the first level curly brace indices from a string. +// It returns an error in case of unbalanced braces. +func braceIndices(s string) ([]int, error) { + var level, idx int + var idxs []int + for i := 0; i < len(s); i++ { + switch s[i] { + case '{': + if level++; level == 1 { + idx = i + } + case '}': + if level--; level == 0 { + idxs = append(idxs, idx, i+1) + } else if level < 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + } + } + if level != 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + return idxs, nil +} + +// varGroupName builds a capturing group name for the indexed variable. +func varGroupName(idx int) string { + return "v" + strconv.Itoa(idx) +} + +// ---------------------------------------------------------------------------- +// routeRegexpGroup +// ---------------------------------------------------------------------------- + +// routeRegexpGroup groups the route matchers that carry variables. +type routeRegexpGroup struct { + host *routeRegexp + path *routeRegexp + queries []*routeRegexp +} + +// setMatch extracts the variables from the URL once a route matches. +func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { + // Store host variables. + if v.host != nil { + host := getHost(req) + matches := v.host.regexp.FindStringSubmatchIndex(host) + if len(matches) > 0 { + extractVars(host, matches, v.host.varsN, m.Vars) + } + } + path := req.URL.Path + if r.useEncodedPath { + path = req.URL.EscapedPath() + } + // Store path variables. + if v.path != nil { + matches := v.path.regexp.FindStringSubmatchIndex(path) + if len(matches) > 0 { + extractVars(path, matches, v.path.varsN, m.Vars) + // Check if we should redirect. + if v.path.options.strictSlash { + p1 := strings.HasSuffix(path, "/") + p2 := strings.HasSuffix(v.path.template, "/") + if p1 != p2 { + u, _ := url.Parse(req.URL.String()) + if p1 { + u.Path = u.Path[:len(u.Path)-1] + } else { + u.Path += "/" + } + m.Handler = http.RedirectHandler(u.String(), 301) + } + } + } + } + // Store query string variables. + for _, q := range v.queries { + queryURL := q.getURLQuery(req) + matches := q.regexp.FindStringSubmatchIndex(queryURL) + if len(matches) > 0 { + extractVars(queryURL, matches, q.varsN, m.Vars) + } + } +} + +// getHost tries its best to return the request host. +func getHost(r *http.Request) string { + if r.URL.IsAbs() { + return r.URL.Host + } + host := r.Host + // Slice off any port information. + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + return host + +} + +func extractVars(input string, matches []int, names []string, output map[string]string) { + for i, name := range names { + output[name] = input[matches[2*i+2]:matches[2*i+3]] + } +} diff --git a/vendor/github.com/gorilla/mux/route.go b/vendor/github.com/gorilla/mux/route.go new file mode 100644 index 0000000000..a591d73545 --- /dev/null +++ b/vendor/github.com/gorilla/mux/route.go @@ -0,0 +1,763 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "regexp" + "strings" +) + +// Route stores information to match a request and build URLs. +type Route struct { + // Parent where the route was registered (a Router). + parent parentRoute + // Request handler for the route. + handler http.Handler + // List of matchers. + matchers []matcher + // Manager for the variables from host and path. + regexp *routeRegexpGroup + // If true, when the path pattern is "/path/", accessing "/path" will + // redirect to the former and vice versa. + strictSlash bool + // If true, when the path pattern is "/path//to", accessing "/path//to" + // will not redirect + skipClean bool + // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" + useEncodedPath bool + // The scheme used when building URLs. + buildScheme string + // If true, this route never matches: it is only used to build URLs. + buildOnly bool + // The name used to build URLs. + name string + // Error resulted from building a route. + err error + + buildVarsFunc BuildVarsFunc +} + +// SkipClean reports whether path cleaning is enabled for this route via +// Router.SkipClean. +func (r *Route) SkipClean() bool { + return r.skipClean +} + +// Match matches the route against the request. +func (r *Route) Match(req *http.Request, match *RouteMatch) bool { + if r.buildOnly || r.err != nil { + return false + } + + var matchErr error + + // Match everything. + for _, m := range r.matchers { + if matched := m.Match(req, match); !matched { + if _, ok := m.(methodMatcher); ok { + matchErr = ErrMethodMismatch + continue + } + matchErr = nil + return false + } + } + + if matchErr != nil { + match.MatchErr = matchErr + return false + } + + if match.MatchErr == ErrMethodMismatch { + // We found a route which matches request method, clear MatchErr + match.MatchErr = nil + // Then override the mis-matched handler + match.Handler = r.handler + } + + // Yay, we have a match. Let's collect some info about it. + if match.Route == nil { + match.Route = r + } + if match.Handler == nil { + match.Handler = r.handler + } + if match.Vars == nil { + match.Vars = make(map[string]string) + } + + // Set variables. + if r.regexp != nil { + r.regexp.setMatch(req, match, r) + } + return true +} + +// ---------------------------------------------------------------------------- +// Route attributes +// ---------------------------------------------------------------------------- + +// GetError returns an error resulted from building the route, if any. +func (r *Route) GetError() error { + return r.err +} + +// BuildOnly sets the route to never match: it is only used to build URLs. +func (r *Route) BuildOnly() *Route { + r.buildOnly = true + return r +} + +// Handler -------------------------------------------------------------------- + +// Handler sets a handler for the route. +func (r *Route) Handler(handler http.Handler) *Route { + if r.err == nil { + r.handler = handler + } + return r +} + +// HandlerFunc sets a handler function for the route. +func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { + return r.Handler(http.HandlerFunc(f)) +} + +// GetHandler returns the handler for the route, if any. +func (r *Route) GetHandler() http.Handler { + return r.handler +} + +// Name ----------------------------------------------------------------------- + +// Name sets the name for the route, used to build URLs. +// If the name was registered already it will be overwritten. +func (r *Route) Name(name string) *Route { + if r.name != "" { + r.err = fmt.Errorf("mux: route already has name %q, can't set %q", + r.name, name) + } + if r.err == nil { + r.name = name + r.getNamedRoutes()[name] = r + } + return r +} + +// GetName returns the name for the route, if any. +func (r *Route) GetName() string { + return r.name +} + +// ---------------------------------------------------------------------------- +// Matchers +// ---------------------------------------------------------------------------- + +// matcher types try to match a request. +type matcher interface { + Match(*http.Request, *RouteMatch) bool +} + +// addMatcher adds a matcher to the route. +func (r *Route) addMatcher(m matcher) *Route { + if r.err == nil { + r.matchers = append(r.matchers, m) + } + return r +} + +// addRegexpMatcher adds a host or path matcher and builder to a route. +func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error { + if r.err != nil { + return r.err + } + r.regexp = r.getRegexpGroup() + if typ == regexpTypePath || typ == regexpTypePrefix { + if len(tpl) > 0 && tpl[0] != '/' { + return fmt.Errorf("mux: path must start with a slash, got %q", tpl) + } + if r.regexp.path != nil { + tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl + } + } + rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{ + strictSlash: r.strictSlash, + useEncodedPath: r.useEncodedPath, + }) + if err != nil { + return err + } + for _, q := range r.regexp.queries { + if err = uniqueVars(rr.varsN, q.varsN); err != nil { + return err + } + } + if typ == regexpTypeHost { + if r.regexp.path != nil { + if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { + return err + } + } + r.regexp.host = rr + } else { + if r.regexp.host != nil { + if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { + return err + } + } + if typ == regexpTypeQuery { + r.regexp.queries = append(r.regexp.queries, rr) + } else { + r.regexp.path = rr + } + } + r.addMatcher(rr) + return nil +} + +// Headers -------------------------------------------------------------------- + +// headerMatcher matches the request against header values. +type headerMatcher map[string]string + +func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithString(m, r.Header, true) +} + +// Headers adds a matcher for request header values. +// It accepts a sequence of key/value pairs to be matched. For example: +// +// r := mux.NewRouter() +// r.Headers("Content-Type", "application/json", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both request header values match. +// If the value is an empty string, it will match any value if the key is set. +func (r *Route) Headers(pairs ...string) *Route { + if r.err == nil { + var headers map[string]string + headers, r.err = mapFromPairsToString(pairs...) + return r.addMatcher(headerMatcher(headers)) + } + return r +} + +// headerRegexMatcher matches the request against the route given a regex for the header +type headerRegexMatcher map[string]*regexp.Regexp + +func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithRegex(m, r.Header, true) +} + +// HeadersRegexp accepts a sequence of key/value pairs, where the value has regex +// support. For example: +// +// r := mux.NewRouter() +// r.HeadersRegexp("Content-Type", "application/(text|json)", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both the request header matches both regular expressions. +// If the value is an empty string, it will match any value if the key is set. +// Use the start and end of string anchors (^ and $) to match an exact value. +func (r *Route) HeadersRegexp(pairs ...string) *Route { + if r.err == nil { + var headers map[string]*regexp.Regexp + headers, r.err = mapFromPairsToRegex(pairs...) + return r.addMatcher(headerRegexMatcher(headers)) + } + return r +} + +// Host ----------------------------------------------------------------------- + +// Host adds a matcher for the URL host. +// It accepts a template with zero or more URL variables enclosed by {}. +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next dot. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter() +// r.Host("www.example.com") +// r.Host("{subdomain}.domain.com") +// r.Host("{subdomain:[a-z]+}.domain.com") +// +// Variable names must be unique in a given route. They can be retrieved +// calling mux.Vars(request). +func (r *Route) Host(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypeHost) + return r +} + +// MatcherFunc ---------------------------------------------------------------- + +// MatcherFunc is the function signature used by custom matchers. +type MatcherFunc func(*http.Request, *RouteMatch) bool + +// Match returns the match for a given request. +func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { + return m(r, match) +} + +// MatcherFunc adds a custom function to be used as request matcher. +func (r *Route) MatcherFunc(f MatcherFunc) *Route { + return r.addMatcher(f) +} + +// Methods -------------------------------------------------------------------- + +// methodMatcher matches the request against HTTP methods. +type methodMatcher []string + +func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchInArray(m, r.Method) +} + +// Methods adds a matcher for HTTP methods. +// It accepts a sequence of one or more methods to be matched, e.g.: +// "GET", "POST", "PUT". +func (r *Route) Methods(methods ...string) *Route { + for k, v := range methods { + methods[k] = strings.ToUpper(v) + } + return r.addMatcher(methodMatcher(methods)) +} + +// Path ----------------------------------------------------------------------- + +// Path adds a matcher for the URL path. +// It accepts a template with zero or more URL variables enclosed by {}. The +// template must start with a "/". +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next slash. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter() +// r.Path("/products/").Handler(ProductsHandler) +// r.Path("/products/{key}").Handler(ProductsHandler) +// r.Path("/articles/{category}/{id:[0-9]+}"). +// Handler(ArticleHandler) +// +// Variable names must be unique in a given route. They can be retrieved +// calling mux.Vars(request). +func (r *Route) Path(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypePath) + return r +} + +// PathPrefix ----------------------------------------------------------------- + +// PathPrefix adds a matcher for the URL path prefix. This matches if the given +// template is a prefix of the full URL path. See Route.Path() for details on +// the tpl argument. +// +// Note that it does not treat slashes specially ("/foobar/" will be matched by +// the prefix "/foo") so you may want to use a trailing slash here. +// +// Also note that the setting of Router.StrictSlash() has no effect on routes +// with a PathPrefix matcher. +func (r *Route) PathPrefix(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypePrefix) + return r +} + +// Query ---------------------------------------------------------------------- + +// Queries adds a matcher for URL query values. +// It accepts a sequence of key/value pairs. Values may define variables. +// For example: +// +// r := mux.NewRouter() +// r.Queries("foo", "bar", "id", "{id:[0-9]+}") +// +// The above route will only match if the URL contains the defined queries +// values, e.g.: ?foo=bar&id=42. +// +// It the value is an empty string, it will match any value if the key is set. +// +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next slash. +// +// - {name:pattern} matches the given regexp pattern. +func (r *Route) Queries(pairs ...string) *Route { + length := len(pairs) + if length%2 != 0 { + r.err = fmt.Errorf( + "mux: number of parameters must be multiple of 2, got %v", pairs) + return nil + } + for i := 0; i < length; i += 2 { + if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], regexpTypeQuery); r.err != nil { + return r + } + } + + return r +} + +// Schemes -------------------------------------------------------------------- + +// schemeMatcher matches the request against URL schemes. +type schemeMatcher []string + +func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchInArray(m, r.URL.Scheme) +} + +// Schemes adds a matcher for URL schemes. +// It accepts a sequence of schemes to be matched, e.g.: "http", "https". +func (r *Route) Schemes(schemes ...string) *Route { + for k, v := range schemes { + schemes[k] = strings.ToLower(v) + } + if r.buildScheme == "" && len(schemes) > 0 { + r.buildScheme = schemes[0] + } + return r.addMatcher(schemeMatcher(schemes)) +} + +// BuildVarsFunc -------------------------------------------------------------- + +// BuildVarsFunc is the function signature used by custom build variable +// functions (which can modify route variables before a route's URL is built). +type BuildVarsFunc func(map[string]string) map[string]string + +// BuildVarsFunc adds a custom function to be used to modify build variables +// before a route's URL is built. +func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { + r.buildVarsFunc = f + return r +} + +// Subrouter ------------------------------------------------------------------ + +// Subrouter creates a subrouter for the route. +// +// It will test the inner routes only if the parent route matched. For example: +// +// r := mux.NewRouter() +// s := r.Host("www.example.com").Subrouter() +// s.HandleFunc("/products/", ProductsHandler) +// s.HandleFunc("/products/{key}", ProductHandler) +// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) +// +// Here, the routes registered in the subrouter won't be tested if the host +// doesn't match. +func (r *Route) Subrouter() *Router { + router := &Router{parent: r, strictSlash: r.strictSlash} + r.addMatcher(router) + return router +} + +// ---------------------------------------------------------------------------- +// URL building +// ---------------------------------------------------------------------------- + +// URL builds a URL for the route. +// +// It accepts a sequence of key/value pairs for the route variables. For +// example, given this route: +// +// r := mux.NewRouter() +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Name("article") +// +// ...a URL for it can be built using: +// +// url, err := r.Get("article").URL("category", "technology", "id", "42") +// +// ...which will return an url.URL with the following path: +// +// "/articles/technology/42" +// +// This also works for host variables: +// +// r := mux.NewRouter() +// r.Host("{subdomain}.domain.com"). +// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Name("article") +// +// // url.String() will be "http://news.domain.com/articles/technology/42" +// url, err := r.Get("article").URL("subdomain", "news", +// "category", "technology", +// "id", "42") +// +// All variables defined in the route are required, and their values must +// conform to the corresponding patterns. +func (r *Route) URL(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp == nil { + return nil, errors.New("mux: route doesn't have a host or path") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + var scheme, host, path string + queries := make([]string, 0, len(r.regexp.queries)) + if r.regexp.host != nil { + if host, err = r.regexp.host.url(values); err != nil { + return nil, err + } + scheme = "http" + if s := r.getBuildScheme(); s != "" { + scheme = s + } + } + if r.regexp.path != nil { + if path, err = r.regexp.path.url(values); err != nil { + return nil, err + } + } + for _, q := range r.regexp.queries { + var query string + if query, err = q.url(values); err != nil { + return nil, err + } + queries = append(queries, query) + } + return &url.URL{ + Scheme: scheme, + Host: host, + Path: path, + RawQuery: strings.Join(queries, "&"), + }, nil +} + +// URLHost builds the host part of the URL for a route. See Route.URL(). +// +// The route must have a host defined. +func (r *Route) URLHost(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp == nil || r.regexp.host == nil { + return nil, errors.New("mux: route doesn't have a host") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + host, err := r.regexp.host.url(values) + if err != nil { + return nil, err + } + u := &url.URL{ + Scheme: "http", + Host: host, + } + if s := r.getBuildScheme(); s != "" { + u.Scheme = s + } + return u, nil +} + +// URLPath builds the path part of the URL for a route. See Route.URL(). +// +// The route must have a path defined. +func (r *Route) URLPath(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp == nil || r.regexp.path == nil { + return nil, errors.New("mux: route doesn't have a path") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + path, err := r.regexp.path.url(values) + if err != nil { + return nil, err + } + return &url.URL{ + Path: path, + }, nil +} + +// GetPathTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp == nil || r.regexp.path == nil { + return "", errors.New("mux: route doesn't have a path") + } + return r.regexp.path.template, nil +} + +// GetPathRegexp returns the expanded regular expression used to match route path. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathRegexp() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp == nil || r.regexp.path == nil { + return "", errors.New("mux: route does not have a path") + } + return r.regexp.path.regexp.String(), nil +} + +// GetQueriesRegexp returns the expanded regular expressions used to match the +// route queries. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not have queries. +func (r *Route) GetQueriesRegexp() ([]string, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp == nil || r.regexp.queries == nil { + return nil, errors.New("mux: route doesn't have queries") + } + var queries []string + for _, query := range r.regexp.queries { + queries = append(queries, query.regexp.String()) + } + return queries, nil +} + +// GetQueriesTemplates returns the templates used to build the +// query matching. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define queries. +func (r *Route) GetQueriesTemplates() ([]string, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp == nil || r.regexp.queries == nil { + return nil, errors.New("mux: route doesn't have queries") + } + var queries []string + for _, query := range r.regexp.queries { + queries = append(queries, query.template) + } + return queries, nil +} + +// GetMethods returns the methods the route matches against +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if route does not have methods. +func (r *Route) GetMethods() ([]string, error) { + if r.err != nil { + return nil, r.err + } + for _, m := range r.matchers { + if methods, ok := m.(methodMatcher); ok { + return []string(methods), nil + } + } + return nil, errors.New("mux: route doesn't have methods") +} + +// GetHostTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a host. +func (r *Route) GetHostTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp == nil || r.regexp.host == nil { + return "", errors.New("mux: route doesn't have a host") + } + return r.regexp.host.template, nil +} + +// prepareVars converts the route variable pairs into a map. If the route has a +// BuildVarsFunc, it is invoked. +func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { + m, err := mapFromPairsToString(pairs...) + if err != nil { + return nil, err + } + return r.buildVars(m), nil +} + +func (r *Route) buildVars(m map[string]string) map[string]string { + if r.parent != nil { + m = r.parent.buildVars(m) + } + if r.buildVarsFunc != nil { + m = r.buildVarsFunc(m) + } + return m +} + +// ---------------------------------------------------------------------------- +// parentRoute +// ---------------------------------------------------------------------------- + +// parentRoute allows routes to know about parent host and path definitions. +type parentRoute interface { + getBuildScheme() string + getNamedRoutes() map[string]*Route + getRegexpGroup() *routeRegexpGroup + buildVars(map[string]string) map[string]string +} + +func (r *Route) getBuildScheme() string { + if r.buildScheme != "" { + return r.buildScheme + } + if r.parent != nil { + return r.parent.getBuildScheme() + } + return "" +} + +// getNamedRoutes returns the map where named routes are registered. +func (r *Route) getNamedRoutes() map[string]*Route { + if r.parent == nil { + // During tests router is not always set. + r.parent = NewRouter() + } + return r.parent.getNamedRoutes() +} + +// getRegexpGroup returns regexp definitions from this route. +func (r *Route) getRegexpGroup() *routeRegexpGroup { + if r.regexp == nil { + if r.parent == nil { + // During tests router is not always set. + r.parent = NewRouter() + } + regexp := r.parent.getRegexpGroup() + if regexp == nil { + r.regexp = new(routeRegexpGroup) + } else { + // Copy. + r.regexp = &routeRegexpGroup{ + host: regexp.host, + path: regexp.path, + queries: regexp.queries, + } + } + } + return r.regexp +} diff --git a/vendor/github.com/gorilla/mux/test_helpers.go b/vendor/github.com/gorilla/mux/test_helpers.go new file mode 100644 index 0000000000..32ecffde48 --- /dev/null +++ b/vendor/github.com/gorilla/mux/test_helpers.go @@ -0,0 +1,19 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import "net/http" + +// SetURLVars sets the URL variables for the given request, to be accessed via +// mux.Vars for testing route behaviour. Arguments are not modified, a shallow +// copy is returned. +// +// This API should only be used for testing purposes; it provides a way to +// inject variables into the request context. Alternatively, URL variables +// can be set by making a route that captures the required variables, +// starting a server and sending the request to that server. +func SetURLVars(r *http.Request, val map[string]string) *http.Request { + return setVars(r, val) +} diff --git a/vendor/github.com/gorilla/websocket/.gitignore b/vendor/github.com/gorilla/websocket/.gitignore new file mode 100644 index 0000000000..cd3fcd1ef7 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/.gitignore @@ -0,0 +1,25 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe + +.idea/ +*.iml diff --git a/vendor/github.com/gorilla/websocket/.travis.yml b/vendor/github.com/gorilla/websocket/.travis.yml new file mode 100644 index 0000000000..a49db51c43 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/.travis.yml @@ -0,0 +1,19 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.7.x + - go: 1.8.x + - go: 1.9.x + - go: 1.10.x + - go: 1.11.x + - go: tip + allow_failures: + - go: tip + +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d .) + - go vet $(go list ./... | grep -v /vendor/) + - go test -v -race ./... diff --git a/vendor/github.com/gorilla/websocket/AUTHORS b/vendor/github.com/gorilla/websocket/AUTHORS new file mode 100644 index 0000000000..1931f40068 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/AUTHORS @@ -0,0 +1,9 @@ +# This is the official list of Gorilla WebSocket authors for copyright +# purposes. +# +# Please keep the list sorted. + +Gary Burd +Google LLC (https://opensource.google.com/) +Joachim Bauch + diff --git a/vendor/github.com/gorilla/websocket/LICENSE b/vendor/github.com/gorilla/websocket/LICENSE new file mode 100644 index 0000000000..9171c97225 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/websocket/README.md b/vendor/github.com/gorilla/websocket/README.md new file mode 100644 index 0000000000..20e391f865 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/README.md @@ -0,0 +1,64 @@ +# Gorilla WebSocket + +Gorilla WebSocket is a [Go](http://golang.org/) implementation of the +[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. + +[![Build Status](https://travis-ci.org/gorilla/websocket.svg?branch=master)](https://travis-ci.org/gorilla/websocket) +[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket) + +### Documentation + +* [API Reference](http://godoc.org/github.com/gorilla/websocket) +* [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) +* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch) + +### Status + +The Gorilla WebSocket package provides a complete and tested implementation of +the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The +package API is stable. + +### Installation + + go get github.com/gorilla/websocket + +### Protocol Compliance + +The Gorilla WebSocket package passes the server tests in the [Autobahn Test +Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn +subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). + +### Gorilla WebSocket compared with other packages + + + + + + + + + + + + + + + + + + +
github.com/gorillagolang.org/x/net
RFC 6455 Features
Passes Autobahn Test SuiteYesNo
Receive fragmented messageYesNo, see note 1
Send close messageYesNo
Send pings and receive pongsYesNo
Get the type of a received data messageYesYes, see note 2
Other Features
Compression ExtensionsExperimentalNo
Read message using io.ReaderYesNo, see note 3
Write message using io.WriteCloserYesNo, see note 3
+ +Notes: + +1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html). +2. The application can get the type of a received data message by implementing + a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal) + function. +3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries. + Read returns when the input buffer is full or a frame boundary is + encountered. Each call to Write sends a single frame message. The Gorilla + io.Reader and io.WriteCloser operate on a single WebSocket message. + diff --git a/vendor/github.com/gorilla/websocket/client.go b/vendor/github.com/gorilla/websocket/client.go new file mode 100644 index 0000000000..2e32fd506e --- /dev/null +++ b/vendor/github.com/gorilla/websocket/client.go @@ -0,0 +1,395 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httptrace" + "net/url" + "strings" + "time" +) + +// ErrBadHandshake is returned when the server response to opening handshake is +// invalid. +var ErrBadHandshake = errors.New("websocket: bad handshake") + +var errInvalidCompression = errors.New("websocket: invalid compression negotiation") + +// NewClient creates a new client connection using the given net connection. +// The URL u specifies the host and request URI. Use requestHeader to specify +// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies +// (Cookie). Use the response.Header to get the selected subprotocol +// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etc. +// +// Deprecated: Use Dialer instead. +func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) { + d := Dialer{ + ReadBufferSize: readBufSize, + WriteBufferSize: writeBufSize, + NetDial: func(net, addr string) (net.Conn, error) { + return netConn, nil + }, + } + return d.Dial(u.String(), requestHeader) +} + +// A Dialer contains options for connecting to WebSocket server. +type Dialer struct { + // NetDial specifies the dial function for creating TCP connections. If + // NetDial is nil, net.Dial is used. + NetDial func(network, addr string) (net.Conn, error) + + // NetDialContext specifies the dial function for creating TCP connections. If + // NetDialContext is nil, net.DialContext is used. + NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error) + + // Proxy specifies a function to return a proxy for a given + // Request. If the function returns a non-nil error, the + // request is aborted with the provided error. + // If Proxy is nil or returns a nil *URL, no proxy is used. + Proxy func(*http.Request) (*url.URL, error) + + // TLSClientConfig specifies the TLS configuration to use with tls.Client. + // If nil, the default configuration is used. + TLSClientConfig *tls.Config + + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer + // size is zero, then a useful default size is used. The I/O buffer sizes + // do not limit the size of the messages that can be sent or received. + ReadBufferSize, WriteBufferSize int + + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + + // Subprotocols specifies the client's requested subprotocols. + Subprotocols []string + + // EnableCompression specifies if the client should attempt to negotiate + // per message compression (RFC 7692). Setting this value to true does not + // guarantee that compression will be supported. Currently only "no context + // takeover" modes are supported. + EnableCompression bool + + // Jar specifies the cookie jar. + // If Jar is nil, cookies are not sent in requests and ignored + // in responses. + Jar http.CookieJar +} + +// Dial creates a new client connection by calling DialContext with a background context. +func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + return d.DialContext(context.Background(), urlStr, requestHeader) +} + +var errMalformedURL = errors.New("malformed ws or wss URL") + +func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { + hostPort = u.Host + hostNoPort = u.Host + if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") { + hostNoPort = hostNoPort[:i] + } else { + switch u.Scheme { + case "wss": + hostPort += ":443" + case "https": + hostPort += ":443" + default: + hostPort += ":80" + } + } + return hostPort, hostNoPort +} + +// DefaultDialer is a dialer with all fields set to the default values. +var DefaultDialer = &Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, +} + +// nilDialer is dialer to use when receiver is nil. +var nilDialer = *DefaultDialer + +// DialContext creates a new client connection. Use requestHeader to specify the +// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). +// Use the response.Header to get the selected subprotocol +// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). +// +// The context will be used in the request and in the Dialer +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etcetera. The response body may not contain the entire response and does not +// need to be closed by the application. +func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + if d == nil { + d = &nilDialer + } + + challengeKey, err := generateChallengeKey() + if err != nil { + return nil, nil, err + } + + u, err := url.Parse(urlStr) + if err != nil { + return nil, nil, err + } + + switch u.Scheme { + case "ws": + u.Scheme = "http" + case "wss": + u.Scheme = "https" + default: + return nil, nil, errMalformedURL + } + + if u.User != nil { + // User name and password are not allowed in websocket URIs. + return nil, nil, errMalformedURL + } + + req := &http.Request{ + Method: "GET", + URL: u, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Host: u.Host, + } + req = req.WithContext(ctx) + + // Set the cookies present in the cookie jar of the dialer + if d.Jar != nil { + for _, cookie := range d.Jar.Cookies(u) { + req.AddCookie(cookie) + } + } + + // Set the request headers using the capitalization for names and values in + // RFC examples. Although the capitalization shouldn't matter, there are + // servers that depend on it. The Header.Set method is not used because the + // method canonicalizes the header names. + req.Header["Upgrade"] = []string{"websocket"} + req.Header["Connection"] = []string{"Upgrade"} + req.Header["Sec-WebSocket-Key"] = []string{challengeKey} + req.Header["Sec-WebSocket-Version"] = []string{"13"} + if len(d.Subprotocols) > 0 { + req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")} + } + for k, vs := range requestHeader { + switch { + case k == "Host": + if len(vs) > 0 { + req.Host = vs[0] + } + case k == "Upgrade" || + k == "Connection" || + k == "Sec-Websocket-Key" || + k == "Sec-Websocket-Version" || + k == "Sec-Websocket-Extensions" || + (k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0): + return nil, nil, errors.New("websocket: duplicate header not allowed: " + k) + case k == "Sec-Websocket-Protocol": + req.Header["Sec-WebSocket-Protocol"] = vs + default: + req.Header[k] = vs + } + } + + if d.EnableCompression { + req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"} + } + + if d.HandshakeTimeout != 0 { + var cancel func() + ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout) + defer cancel() + } + + // Get network dial function. + var netDial func(network, add string) (net.Conn, error) + + if d.NetDialContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialContext(ctx, network, addr) + } + } else if d.NetDial != nil { + netDial = d.NetDial + } else { + netDialer := &net.Dialer{} + netDial = func(network, addr string) (net.Conn, error) { + return netDialer.DialContext(ctx, network, addr) + } + } + + // If needed, wrap the dial function to set the connection deadline. + if deadline, ok := ctx.Deadline(); ok { + forwardDial := netDial + netDial = func(network, addr string) (net.Conn, error) { + c, err := forwardDial(network, addr) + if err != nil { + return nil, err + } + err = c.SetDeadline(deadline) + if err != nil { + c.Close() + return nil, err + } + return c, nil + } + } + + // If needed, wrap the dial function to connect through a proxy. + if d.Proxy != nil { + proxyURL, err := d.Proxy(req) + if err != nil { + return nil, nil, err + } + if proxyURL != nil { + dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial)) + if err != nil { + return nil, nil, err + } + netDial = dialer.Dial + } + } + + hostPort, hostNoPort := hostPortNoPort(u) + trace := httptrace.ContextClientTrace(ctx) + if trace != nil && trace.GetConn != nil { + trace.GetConn(hostPort) + } + + netConn, err := netDial("tcp", hostPort) + if trace != nil && trace.GotConn != nil { + trace.GotConn(httptrace.GotConnInfo{ + Conn: netConn, + }) + } + if err != nil { + return nil, nil, err + } + + defer func() { + if netConn != nil { + netConn.Close() + } + }() + + if u.Scheme == "https" { + cfg := cloneTLSConfig(d.TLSClientConfig) + if cfg.ServerName == "" { + cfg.ServerName = hostNoPort + } + tlsConn := tls.Client(netConn, cfg) + netConn = tlsConn + + var err error + if trace != nil { + err = doHandshakeWithTrace(trace, tlsConn, cfg) + } else { + err = doHandshake(tlsConn, cfg) + } + + if err != nil { + return nil, nil, err + } + } + + conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil) + + if err := req.Write(netConn); err != nil { + return nil, nil, err + } + + if trace != nil && trace.GotFirstResponseByte != nil { + if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 { + trace.GotFirstResponseByte() + } + } + + resp, err := http.ReadResponse(conn.br, req) + if err != nil { + return nil, nil, err + } + + if d.Jar != nil { + if rc := resp.Cookies(); len(rc) > 0 { + d.Jar.SetCookies(u, rc) + } + } + + if resp.StatusCode != 101 || + !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") || + !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") || + resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) { + // Before closing the network connection on return from this + // function, slurp up some of the response to aid application + // debugging. + buf := make([]byte, 1024) + n, _ := io.ReadFull(resp.Body, buf) + resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n])) + return nil, resp, ErrBadHandshake + } + + for _, ext := range parseExtensions(resp.Header) { + if ext[""] != "permessage-deflate" { + continue + } + _, snct := ext["server_no_context_takeover"] + _, cnct := ext["client_no_context_takeover"] + if !snct || !cnct { + return nil, resp, errInvalidCompression + } + conn.newCompressionWriter = compressNoContextTakeover + conn.newDecompressionReader = decompressNoContextTakeover + break + } + + resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) + conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") + + netConn.SetDeadline(time.Time{}) + netConn = nil // to avoid close in defer. + return conn, resp, nil +} + +func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error { + if err := tlsConn.Handshake(); err != nil { + return err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/client_clone.go b/vendor/github.com/gorilla/websocket/client_clone.go new file mode 100644 index 0000000000..4f0d943723 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/client_clone.go @@ -0,0 +1,16 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.8 + +package websocket + +import "crypto/tls" + +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} + } + return cfg.Clone() +} diff --git a/vendor/github.com/gorilla/websocket/client_clone_legacy.go b/vendor/github.com/gorilla/websocket/client_clone_legacy.go new file mode 100644 index 0000000000..babb007fb4 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/client_clone_legacy.go @@ -0,0 +1,38 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.8 + +package websocket + +import "crypto/tls" + +// cloneTLSConfig clones all public fields except the fields +// SessionTicketsDisabled and SessionTicketKey. This avoids copying the +// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a +// config in active use. +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &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, + } +} diff --git a/vendor/github.com/gorilla/websocket/compression.go b/vendor/github.com/gorilla/websocket/compression.go new file mode 100644 index 0000000000..813ffb1e84 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/compression.go @@ -0,0 +1,148 @@ +// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "compress/flate" + "errors" + "io" + "strings" + "sync" +) + +const ( + minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6 + maxCompressionLevel = flate.BestCompression + defaultCompressionLevel = 1 +) + +var ( + flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool + flateReaderPool = sync.Pool{New: func() interface{} { + return flate.NewReader(nil) + }} +) + +func decompressNoContextTakeover(r io.Reader) io.ReadCloser { + const tail = + // Add four bytes as specified in RFC + "\x00\x00\xff\xff" + + // Add final block to squelch unexpected EOF error from flate reader. + "\x01\x00\x00\xff\xff" + + fr, _ := flateReaderPool.Get().(io.ReadCloser) + fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil) + return &flateReadWrapper{fr} +} + +func isValidCompressionLevel(level int) bool { + return minCompressionLevel <= level && level <= maxCompressionLevel +} + +func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser { + p := &flateWriterPools[level-minCompressionLevel] + tw := &truncWriter{w: w} + fw, _ := p.Get().(*flate.Writer) + if fw == nil { + fw, _ = flate.NewWriter(tw, level) + } else { + fw.Reset(tw) + } + return &flateWriteWrapper{fw: fw, tw: tw, p: p} +} + +// truncWriter is an io.Writer that writes all but the last four bytes of the +// stream to another io.Writer. +type truncWriter struct { + w io.WriteCloser + n int + p [4]byte +} + +func (w *truncWriter) Write(p []byte) (int, error) { + n := 0 + + // fill buffer first for simplicity. + if w.n < len(w.p) { + n = copy(w.p[w.n:], p) + p = p[n:] + w.n += n + if len(p) == 0 { + return n, nil + } + } + + m := len(p) + if m > len(w.p) { + m = len(w.p) + } + + if nn, err := w.w.Write(w.p[:m]); err != nil { + return n + nn, err + } + + copy(w.p[:], w.p[m:]) + copy(w.p[len(w.p)-m:], p[len(p)-m:]) + nn, err := w.w.Write(p[:len(p)-m]) + return n + nn, err +} + +type flateWriteWrapper struct { + fw *flate.Writer + tw *truncWriter + p *sync.Pool +} + +func (w *flateWriteWrapper) Write(p []byte) (int, error) { + if w.fw == nil { + return 0, errWriteClosed + } + return w.fw.Write(p) +} + +func (w *flateWriteWrapper) Close() error { + if w.fw == nil { + return errWriteClosed + } + err1 := w.fw.Flush() + w.p.Put(w.fw) + w.fw = nil + if w.tw.p != [4]byte{0, 0, 0xff, 0xff} { + return errors.New("websocket: internal error, unexpected bytes at end of flate stream") + } + err2 := w.tw.w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +type flateReadWrapper struct { + fr io.ReadCloser +} + +func (r *flateReadWrapper) Read(p []byte) (int, error) { + if r.fr == nil { + return 0, io.ErrClosedPipe + } + n, err := r.fr.Read(p) + if err == io.EOF { + // Preemptively place the reader back in the pool. This helps with + // scenarios where the application does not call NextReader() soon after + // this final read. + r.Close() + } + return n, err +} + +func (r *flateReadWrapper) Close() error { + if r.fr == nil { + return io.ErrClosedPipe + } + err := r.fr.Close() + flateReaderPool.Put(r.fr) + r.fr = nil + return err +} diff --git a/vendor/github.com/gorilla/websocket/conn.go b/vendor/github.com/gorilla/websocket/conn.go new file mode 100644 index 0000000000..d2a21c148b --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -0,0 +1,1165 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "encoding/binary" + "errors" + "io" + "io/ioutil" + "math/rand" + "net" + "strconv" + "sync" + "time" + "unicode/utf8" +) + +const ( + // Frame header byte 0 bits from Section 5.2 of RFC 6455 + finalBit = 1 << 7 + rsv1Bit = 1 << 6 + rsv2Bit = 1 << 5 + rsv3Bit = 1 << 4 + + // Frame header byte 1 bits from Section 5.2 of RFC 6455 + maskBit = 1 << 7 + + maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask + maxControlFramePayloadSize = 125 + + writeWait = time.Second + + defaultReadBufferSize = 4096 + defaultWriteBufferSize = 4096 + + continuationFrame = 0 + noFrame = -1 +) + +// Close codes defined in RFC 6455, section 11.7. +const ( + CloseNormalClosure = 1000 + CloseGoingAway = 1001 + CloseProtocolError = 1002 + CloseUnsupportedData = 1003 + CloseNoStatusReceived = 1005 + CloseAbnormalClosure = 1006 + CloseInvalidFramePayloadData = 1007 + ClosePolicyViolation = 1008 + CloseMessageTooBig = 1009 + CloseMandatoryExtension = 1010 + CloseInternalServerErr = 1011 + CloseServiceRestart = 1012 + CloseTryAgainLater = 1013 + CloseTLSHandshake = 1015 +) + +// The message types are defined in RFC 6455, section 11.8. +const ( + // TextMessage denotes a text data message. The text message payload is + // interpreted as UTF-8 encoded text data. + TextMessage = 1 + + // BinaryMessage denotes a binary data message. + BinaryMessage = 2 + + // CloseMessage denotes a close control message. The optional message + // payload contains a numeric code and text. Use the FormatCloseMessage + // function to format a close message payload. + CloseMessage = 8 + + // PingMessage denotes a ping control message. The optional message payload + // is UTF-8 encoded text. + PingMessage = 9 + + // PongMessage denotes a pong control message. The optional message payload + // is UTF-8 encoded text. + PongMessage = 10 +) + +// ErrCloseSent is returned when the application writes a message to the +// connection after sending a close message. +var ErrCloseSent = errors.New("websocket: close sent") + +// ErrReadLimit is returned when reading a message that is larger than the +// read limit set for the connection. +var ErrReadLimit = errors.New("websocket: read limit exceeded") + +// netError satisfies the net Error interface. +type netError struct { + msg string + temporary bool + timeout bool +} + +func (e *netError) Error() string { return e.msg } +func (e *netError) Temporary() bool { return e.temporary } +func (e *netError) Timeout() bool { return e.timeout } + +// CloseError represents a close message. +type CloseError struct { + // Code is defined in RFC 6455, section 11.7. + Code int + + // Text is the optional text payload. + Text string +} + +func (e *CloseError) Error() string { + s := []byte("websocket: close ") + s = strconv.AppendInt(s, int64(e.Code), 10) + switch e.Code { + case CloseNormalClosure: + s = append(s, " (normal)"...) + case CloseGoingAway: + s = append(s, " (going away)"...) + case CloseProtocolError: + s = append(s, " (protocol error)"...) + case CloseUnsupportedData: + s = append(s, " (unsupported data)"...) + case CloseNoStatusReceived: + s = append(s, " (no status)"...) + case CloseAbnormalClosure: + s = append(s, " (abnormal closure)"...) + case CloseInvalidFramePayloadData: + s = append(s, " (invalid payload data)"...) + case ClosePolicyViolation: + s = append(s, " (policy violation)"...) + case CloseMessageTooBig: + s = append(s, " (message too big)"...) + case CloseMandatoryExtension: + s = append(s, " (mandatory extension missing)"...) + case CloseInternalServerErr: + s = append(s, " (internal server error)"...) + case CloseTLSHandshake: + s = append(s, " (TLS handshake error)"...) + } + if e.Text != "" { + s = append(s, ": "...) + s = append(s, e.Text...) + } + return string(s) +} + +// IsCloseError returns boolean indicating whether the error is a *CloseError +// with one of the specified codes. +func IsCloseError(err error, codes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range codes { + if e.Code == code { + return true + } + } + } + return false +} + +// IsUnexpectedCloseError returns boolean indicating whether the error is a +// *CloseError with a code not in the list of expected codes. +func IsUnexpectedCloseError(err error, expectedCodes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range expectedCodes { + if e.Code == code { + return false + } + } + return true + } + return false +} + +var ( + errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true, temporary: true} + errUnexpectedEOF = &CloseError{Code: CloseAbnormalClosure, Text: io.ErrUnexpectedEOF.Error()} + errBadWriteOpCode = errors.New("websocket: bad write message type") + errWriteClosed = errors.New("websocket: write closed") + errInvalidControlFrame = errors.New("websocket: invalid control frame") +) + +func newMaskKey() [4]byte { + n := rand.Uint32() + return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} +} + +func hideTempErr(err error) error { + if e, ok := err.(net.Error); ok && e.Temporary() { + err = &netError{msg: e.Error(), timeout: e.Timeout()} + } + return err +} + +func isControl(frameType int) bool { + return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage +} + +func isData(frameType int) bool { + return frameType == TextMessage || frameType == BinaryMessage +} + +var validReceivedCloseCodes = map[int]bool{ + // see http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number + + CloseNormalClosure: true, + CloseGoingAway: true, + CloseProtocolError: true, + CloseUnsupportedData: true, + CloseNoStatusReceived: false, + CloseAbnormalClosure: false, + CloseInvalidFramePayloadData: true, + ClosePolicyViolation: true, + CloseMessageTooBig: true, + CloseMandatoryExtension: true, + CloseInternalServerErr: true, + CloseServiceRestart: true, + CloseTryAgainLater: true, + CloseTLSHandshake: false, +} + +func isValidReceivedCloseCode(code int) bool { + return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999) +} + +// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this +// interface. The type of the value stored in a pool is not specified. +type BufferPool interface { + // Get gets a value from the pool or returns nil if the pool is empty. + Get() interface{} + // Put adds a value to the pool. + Put(interface{}) +} + +// writePoolData is the type added to the write buffer pool. This wrapper is +// used to prevent applications from peeking at and depending on the values +// added to the pool. +type writePoolData struct{ buf []byte } + +// The Conn type represents a WebSocket connection. +type Conn struct { + conn net.Conn + isServer bool + subprotocol string + + // Write fields + mu chan bool // used as mutex to protect write to conn + writeBuf []byte // frame is constructed in this buffer. + writePool BufferPool + writeBufSize int + writeDeadline time.Time + writer io.WriteCloser // the current writer returned to the application + isWriting bool // for best-effort concurrent write detection + + writeErrMu sync.Mutex + writeErr error + + enableWriteCompression bool + compressionLevel int + newCompressionWriter func(io.WriteCloser, int) io.WriteCloser + + // Read fields + reader io.ReadCloser // the current reader returned to the application + readErr error + br *bufio.Reader + readRemaining int64 // bytes remaining in current frame. + readFinal bool // true the current message has more frames. + readLength int64 // Message size. + readLimit int64 // Maximum message size. + readMaskPos int + readMaskKey [4]byte + handlePong func(string) error + handlePing func(string) error + handleClose func(int, string) error + readErrCount int + messageReader *messageReader // the current low-level reader + + readDecompress bool // whether last read frame had RSV1 set + newDecompressionReader func(io.Reader) io.ReadCloser +} + +func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn { + + if br == nil { + if readBufferSize == 0 { + readBufferSize = defaultReadBufferSize + } else if readBufferSize < maxControlFramePayloadSize { + // must be large enough for control frame + readBufferSize = maxControlFramePayloadSize + } + br = bufio.NewReaderSize(conn, readBufferSize) + } + + if writeBufferSize <= 0 { + writeBufferSize = defaultWriteBufferSize + } + writeBufferSize += maxFrameHeaderSize + + if writeBuf == nil && writeBufferPool == nil { + writeBuf = make([]byte, writeBufferSize) + } + + mu := make(chan bool, 1) + mu <- true + c := &Conn{ + isServer: isServer, + br: br, + conn: conn, + mu: mu, + readFinal: true, + writeBuf: writeBuf, + writePool: writeBufferPool, + writeBufSize: writeBufferSize, + enableWriteCompression: true, + compressionLevel: defaultCompressionLevel, + } + c.SetCloseHandler(nil) + c.SetPingHandler(nil) + c.SetPongHandler(nil) + return c +} + +// Subprotocol returns the negotiated protocol for the connection. +func (c *Conn) Subprotocol() string { + return c.subprotocol +} + +// Close closes the underlying network connection without sending or waiting +// for a close message. +func (c *Conn) Close() error { + return c.conn.Close() +} + +// LocalAddr returns the local network address. +func (c *Conn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +// RemoteAddr returns the remote network address. +func (c *Conn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +// Write methods + +func (c *Conn) writeFatal(err error) error { + err = hideTempErr(err) + c.writeErrMu.Lock() + if c.writeErr == nil { + c.writeErr = err + } + c.writeErrMu.Unlock() + return err +} + +func (c *Conn) read(n int) ([]byte, error) { + p, err := c.br.Peek(n) + if err == io.EOF { + err = errUnexpectedEOF + } + c.br.Discard(len(p)) + return p, err +} + +func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error { + <-c.mu + defer func() { c.mu <- true }() + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err + } + + c.conn.SetWriteDeadline(deadline) + if len(buf1) == 0 { + _, err = c.conn.Write(buf0) + } else { + err = c.writeBufs(buf0, buf1) + } + if err != nil { + return c.writeFatal(err) + } + if frameType == CloseMessage { + c.writeFatal(ErrCloseSent) + } + return nil +} + +// WriteControl writes a control message with the given deadline. The allowed +// message types are CloseMessage, PingMessage and PongMessage. +func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error { + if !isControl(messageType) { + return errBadWriteOpCode + } + if len(data) > maxControlFramePayloadSize { + return errInvalidControlFrame + } + + b0 := byte(messageType) | finalBit + b1 := byte(len(data)) + if !c.isServer { + b1 |= maskBit + } + + buf := make([]byte, 0, maxFrameHeaderSize+maxControlFramePayloadSize) + buf = append(buf, b0, b1) + + if c.isServer { + buf = append(buf, data...) + } else { + key := newMaskKey() + buf = append(buf, key[:]...) + buf = append(buf, data...) + maskBytes(key, 0, buf[6:]) + } + + d := time.Hour * 1000 + if !deadline.IsZero() { + d = deadline.Sub(time.Now()) + if d < 0 { + return errWriteTimeout + } + } + + timer := time.NewTimer(d) + select { + case <-c.mu: + timer.Stop() + case <-timer.C: + return errWriteTimeout + } + defer func() { c.mu <- true }() + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err + } + + c.conn.SetWriteDeadline(deadline) + _, err = c.conn.Write(buf) + if err != nil { + return c.writeFatal(err) + } + if messageType == CloseMessage { + c.writeFatal(ErrCloseSent) + } + return err +} + +func (c *Conn) prepWrite(messageType int) error { + // Close previous writer if not already closed by the application. It's + // probably better to return an error in this situation, but we cannot + // change this without breaking existing applications. + if c.writer != nil { + c.writer.Close() + c.writer = nil + } + + if !isControl(messageType) && !isData(messageType) { + return errBadWriteOpCode + } + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err + } + + if c.writeBuf == nil { + wpd, ok := c.writePool.Get().(writePoolData) + if ok { + c.writeBuf = wpd.buf + } else { + c.writeBuf = make([]byte, c.writeBufSize) + } + } + return nil +} + +// NextWriter returns a writer for the next message to send. The writer's Close +// method flushes the complete message to the network. +// +// There can be at most one open writer on a connection. NextWriter closes the +// previous writer if the application has not already done so. +// +// All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and +// PongMessage) are supported. +func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) { + if err := c.prepWrite(messageType); err != nil { + return nil, err + } + + mw := &messageWriter{ + c: c, + frameType: messageType, + pos: maxFrameHeaderSize, + } + c.writer = mw + if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) { + w := c.newCompressionWriter(c.writer, c.compressionLevel) + mw.compress = true + c.writer = w + } + return c.writer, nil +} + +type messageWriter struct { + c *Conn + compress bool // whether next call to flushFrame should set RSV1 + pos int // end of data in writeBuf. + frameType int // type of the current frame. + err error +} + +func (w *messageWriter) fatal(err error) error { + if w.err != nil { + w.err = err + w.c.writer = nil + } + return err +} + +// flushFrame writes buffered data and extra as a frame to the network. The +// final argument indicates that this is the last frame in the message. +func (w *messageWriter) flushFrame(final bool, extra []byte) error { + c := w.c + length := w.pos - maxFrameHeaderSize + len(extra) + + // Check for invalid control frames. + if isControl(w.frameType) && + (!final || length > maxControlFramePayloadSize) { + return w.fatal(errInvalidControlFrame) + } + + b0 := byte(w.frameType) + if final { + b0 |= finalBit + } + if w.compress { + b0 |= rsv1Bit + } + w.compress = false + + b1 := byte(0) + if !c.isServer { + b1 |= maskBit + } + + // Assume that the frame starts at beginning of c.writeBuf. + framePos := 0 + if c.isServer { + // Adjust up if mask not included in the header. + framePos = 4 + } + + switch { + case length >= 65536: + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 127 + binary.BigEndian.PutUint64(c.writeBuf[framePos+2:], uint64(length)) + case length > 125: + framePos += 6 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 126 + binary.BigEndian.PutUint16(c.writeBuf[framePos+2:], uint16(length)) + default: + framePos += 8 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | byte(length) + } + + if !c.isServer { + key := newMaskKey() + copy(c.writeBuf[maxFrameHeaderSize-4:], key[:]) + maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos]) + if len(extra) > 0 { + return c.writeFatal(errors.New("websocket: internal error, extra used in client mode")) + } + } + + // Write the buffers to the connection with best-effort detection of + // concurrent writes. See the concurrency section in the package + // documentation for more info. + + if c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = true + + err := c.write(w.frameType, c.writeDeadline, c.writeBuf[framePos:w.pos], extra) + + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + + if err != nil { + return w.fatal(err) + } + + if final { + c.writer = nil + if c.writePool != nil { + c.writePool.Put(writePoolData{buf: c.writeBuf}) + c.writeBuf = nil + } + return nil + } + + // Setup for next frame. + w.pos = maxFrameHeaderSize + w.frameType = continuationFrame + return nil +} + +func (w *messageWriter) ncopy(max int) (int, error) { + n := len(w.c.writeBuf) - w.pos + if n <= 0 { + if err := w.flushFrame(false, nil); err != nil { + return 0, err + } + n = len(w.c.writeBuf) - w.pos + } + if n > max { + n = max + } + return n, nil +} + +func (w *messageWriter) Write(p []byte) (int, error) { + if w.err != nil { + return 0, w.err + } + + if len(p) > 2*len(w.c.writeBuf) && w.c.isServer { + // Don't buffer large messages. + err := w.flushFrame(false, p) + if err != nil { + return 0, err + } + return len(p), nil + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.pos:], p[:n]) + w.pos += n + p = p[n:] + } + return nn, nil +} + +func (w *messageWriter) WriteString(p string) (int, error) { + if w.err != nil { + return 0, w.err + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.pos:], p[:n]) + w.pos += n + p = p[n:] + } + return nn, nil +} + +func (w *messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { + if w.err != nil { + return 0, w.err + } + for { + if w.pos == len(w.c.writeBuf) { + err = w.flushFrame(false, nil) + if err != nil { + break + } + } + var n int + n, err = r.Read(w.c.writeBuf[w.pos:]) + w.pos += n + nn += int64(n) + if err != nil { + if err == io.EOF { + err = nil + } + break + } + } + return nn, err +} + +func (w *messageWriter) Close() error { + if w.err != nil { + return w.err + } + if err := w.flushFrame(true, nil); err != nil { + return err + } + w.err = errWriteClosed + return nil +} + +// WritePreparedMessage writes prepared message into connection. +func (c *Conn) WritePreparedMessage(pm *PreparedMessage) error { + frameType, frameData, err := pm.frame(prepareKey{ + isServer: c.isServer, + compress: c.newCompressionWriter != nil && c.enableWriteCompression && isData(pm.messageType), + compressionLevel: c.compressionLevel, + }) + if err != nil { + return err + } + if c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = true + err = c.write(frameType, c.writeDeadline, frameData, nil) + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + return err +} + +// WriteMessage is a helper method for getting a writer using NextWriter, +// writing the message and closing the writer. +func (c *Conn) WriteMessage(messageType int, data []byte) error { + + if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) { + // Fast path with no allocations and single frame. + + if err := c.prepWrite(messageType); err != nil { + return err + } + mw := messageWriter{c: c, frameType: messageType, pos: maxFrameHeaderSize} + n := copy(c.writeBuf[mw.pos:], data) + mw.pos += n + data = data[n:] + return mw.flushFrame(true, data) + } + + w, err := c.NextWriter(messageType) + if err != nil { + return err + } + if _, err = w.Write(data); err != nil { + return err + } + return w.Close() +} + +// SetWriteDeadline sets the write deadline on the underlying network +// connection. After a write has timed out, the websocket state is corrupt and +// all future writes will return an error. A zero value for t means writes will +// not time out. +func (c *Conn) SetWriteDeadline(t time.Time) error { + c.writeDeadline = t + return nil +} + +// Read methods + +func (c *Conn) advanceFrame() (int, error) { + // 1. Skip remainder of previous frame. + + if c.readRemaining > 0 { + if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil { + return noFrame, err + } + } + + // 2. Read and parse first two bytes of frame header. + + p, err := c.read(2) + if err != nil { + return noFrame, err + } + + final := p[0]&finalBit != 0 + frameType := int(p[0] & 0xf) + mask := p[1]&maskBit != 0 + c.readRemaining = int64(p[1] & 0x7f) + + c.readDecompress = false + if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 { + c.readDecompress = true + p[0] &^= rsv1Bit + } + + if rsv := p[0] & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 { + return noFrame, c.handleProtocolError("unexpected reserved bits 0x" + strconv.FormatInt(int64(rsv), 16)) + } + + switch frameType { + case CloseMessage, PingMessage, PongMessage: + if c.readRemaining > maxControlFramePayloadSize { + return noFrame, c.handleProtocolError("control frame length > 125") + } + if !final { + return noFrame, c.handleProtocolError("control frame not final") + } + case TextMessage, BinaryMessage: + if !c.readFinal { + return noFrame, c.handleProtocolError("message start before final message frame") + } + c.readFinal = final + case continuationFrame: + if c.readFinal { + return noFrame, c.handleProtocolError("continuation after final message frame") + } + c.readFinal = final + default: + return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType)) + } + + // 3. Read and parse frame length. + + switch c.readRemaining { + case 126: + p, err := c.read(2) + if err != nil { + return noFrame, err + } + c.readRemaining = int64(binary.BigEndian.Uint16(p)) + case 127: + p, err := c.read(8) + if err != nil { + return noFrame, err + } + c.readRemaining = int64(binary.BigEndian.Uint64(p)) + } + + // 4. Handle frame masking. + + if mask != c.isServer { + return noFrame, c.handleProtocolError("incorrect mask flag") + } + + if mask { + c.readMaskPos = 0 + p, err := c.read(len(c.readMaskKey)) + if err != nil { + return noFrame, err + } + copy(c.readMaskKey[:], p) + } + + // 5. For text and binary messages, enforce read limit and return. + + if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage { + + c.readLength += c.readRemaining + if c.readLimit > 0 && c.readLength > c.readLimit { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait)) + return noFrame, ErrReadLimit + } + + return frameType, nil + } + + // 6. Read control frame payload. + + var payload []byte + if c.readRemaining > 0 { + payload, err = c.read(int(c.readRemaining)) + c.readRemaining = 0 + if err != nil { + return noFrame, err + } + if c.isServer { + maskBytes(c.readMaskKey, 0, payload) + } + } + + // 7. Process control frame payload. + + switch frameType { + case PongMessage: + if err := c.handlePong(string(payload)); err != nil { + return noFrame, err + } + case PingMessage: + if err := c.handlePing(string(payload)); err != nil { + return noFrame, err + } + case CloseMessage: + closeCode := CloseNoStatusReceived + closeText := "" + if len(payload) >= 2 { + closeCode = int(binary.BigEndian.Uint16(payload)) + if !isValidReceivedCloseCode(closeCode) { + return noFrame, c.handleProtocolError("invalid close code") + } + closeText = string(payload[2:]) + if !utf8.ValidString(closeText) { + return noFrame, c.handleProtocolError("invalid utf8 payload in close frame") + } + } + if err := c.handleClose(closeCode, closeText); err != nil { + return noFrame, err + } + return noFrame, &CloseError{Code: closeCode, Text: closeText} + } + + return frameType, nil +} + +func (c *Conn) handleProtocolError(message string) error { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseProtocolError, message), time.Now().Add(writeWait)) + return errors.New("websocket: " + message) +} + +// NextReader returns the next data message received from the peer. The +// returned messageType is either TextMessage or BinaryMessage. +// +// There can be at most one open reader on a connection. NextReader discards +// the previous message if the application has not already consumed it. +// +// Applications must break out of the application's read loop when this method +// returns a non-nil error value. Errors returned from this method are +// permanent. Once this method returns a non-nil error, all subsequent calls to +// this method return the same error. +func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { + // Close previous reader, only relevant for decompression. + if c.reader != nil { + c.reader.Close() + c.reader = nil + } + + c.messageReader = nil + c.readLength = 0 + + for c.readErr == nil { + frameType, err := c.advanceFrame() + if err != nil { + c.readErr = hideTempErr(err) + break + } + if frameType == TextMessage || frameType == BinaryMessage { + c.messageReader = &messageReader{c} + c.reader = c.messageReader + if c.readDecompress { + c.reader = c.newDecompressionReader(c.reader) + } + return frameType, c.reader, nil + } + } + + // Applications that do handle the error returned from this method spin in + // tight loop on connection failure. To help application developers detect + // this error, panic on repeated reads to the failed connection. + c.readErrCount++ + if c.readErrCount >= 1000 { + panic("repeated read on failed websocket connection") + } + + return noFrame, nil, c.readErr +} + +type messageReader struct{ c *Conn } + +func (r *messageReader) Read(b []byte) (int, error) { + c := r.c + if c.messageReader != r { + return 0, io.EOF + } + + for c.readErr == nil { + + if c.readRemaining > 0 { + if int64(len(b)) > c.readRemaining { + b = b[:c.readRemaining] + } + n, err := c.br.Read(b) + c.readErr = hideTempErr(err) + if c.isServer { + c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n]) + } + c.readRemaining -= int64(n) + if c.readRemaining > 0 && c.readErr == io.EOF { + c.readErr = errUnexpectedEOF + } + return n, c.readErr + } + + if c.readFinal { + c.messageReader = nil + return 0, io.EOF + } + + frameType, err := c.advanceFrame() + switch { + case err != nil: + c.readErr = hideTempErr(err) + case frameType == TextMessage || frameType == BinaryMessage: + c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") + } + } + + err := c.readErr + if err == io.EOF && c.messageReader == r { + err = errUnexpectedEOF + } + return 0, err +} + +func (r *messageReader) Close() error { + return nil +} + +// ReadMessage is a helper method for getting a reader using NextReader and +// reading from that reader to a buffer. +func (c *Conn) ReadMessage() (messageType int, p []byte, err error) { + var r io.Reader + messageType, r, err = c.NextReader() + if err != nil { + return messageType, nil, err + } + p, err = ioutil.ReadAll(r) + return messageType, p, err +} + +// SetReadDeadline sets the read deadline on the underlying network connection. +// After a read has timed out, the websocket connection state is corrupt and +// all future reads will return an error. A zero value for t means reads will +// not time out. +func (c *Conn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +// SetReadLimit sets the maximum size for a message read from the peer. If a +// message exceeds the limit, the connection sends a close message to the peer +// and returns ErrReadLimit to the application. +func (c *Conn) SetReadLimit(limit int64) { + c.readLimit = limit +} + +// CloseHandler returns the current close handler +func (c *Conn) CloseHandler() func(code int, text string) error { + return c.handleClose +} + +// SetCloseHandler sets the handler for close messages received from the peer. +// The code argument to h is the received close code or CloseNoStatusReceived +// if the close message is empty. The default close handler sends a close +// message back to the peer. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// close messages as described in the section on Control Messages above. +// +// The connection read methods return a CloseError when a close message is +// received. Most applications should handle close messages as part of their +// normal error handling. Applications should only set a close handler when the +// application must perform some action before sending a close message back to +// the peer. +func (c *Conn) SetCloseHandler(h func(code int, text string) error) { + if h == nil { + h = func(code int, text string) error { + message := FormatCloseMessage(code, "") + c.WriteControl(CloseMessage, message, time.Now().Add(writeWait)) + return nil + } + } + c.handleClose = h +} + +// PingHandler returns the current ping handler +func (c *Conn) PingHandler() func(appData string) error { + return c.handlePing +} + +// SetPingHandler sets the handler for ping messages received from the peer. +// The appData argument to h is the PING message application data. The default +// ping handler sends a pong to the peer. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// ping messages as described in the section on Control Messages above. +func (c *Conn) SetPingHandler(h func(appData string) error) { + if h == nil { + h = func(message string) error { + err := c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait)) + if err == ErrCloseSent { + return nil + } else if e, ok := err.(net.Error); ok && e.Temporary() { + return nil + } + return err + } + } + c.handlePing = h +} + +// PongHandler returns the current pong handler +func (c *Conn) PongHandler() func(appData string) error { + return c.handlePong +} + +// SetPongHandler sets the handler for pong messages received from the peer. +// The appData argument to h is the PONG message application data. The default +// pong handler does nothing. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// pong messages as described in the section on Control Messages above. +func (c *Conn) SetPongHandler(h func(appData string) error) { + if h == nil { + h = func(string) error { return nil } + } + c.handlePong = h +} + +// UnderlyingConn returns the internal net.Conn. This can be used to further +// modifications to connection specific flags. +func (c *Conn) UnderlyingConn() net.Conn { + return c.conn +} + +// EnableWriteCompression enables and disables write compression of +// subsequent text and binary messages. This function is a noop if +// compression was not negotiated with the peer. +func (c *Conn) EnableWriteCompression(enable bool) { + c.enableWriteCompression = enable +} + +// SetCompressionLevel sets the flate compression level for subsequent text and +// binary messages. This function is a noop if compression was not negotiated +// with the peer. See the compress/flate package for a description of +// compression levels. +func (c *Conn) SetCompressionLevel(level int) error { + if !isValidCompressionLevel(level) { + return errors.New("websocket: invalid compression level") + } + c.compressionLevel = level + return nil +} + +// FormatCloseMessage formats closeCode and text as a WebSocket close message. +// An empty message is returned for code CloseNoStatusReceived. +func FormatCloseMessage(closeCode int, text string) []byte { + if closeCode == CloseNoStatusReceived { + // Return empty message because it's illegal to send + // CloseNoStatusReceived. Return non-nil value in case application + // checks for nil. + return []byte{} + } + buf := make([]byte, 2+len(text)) + binary.BigEndian.PutUint16(buf, uint16(closeCode)) + copy(buf[2:], text) + return buf +} diff --git a/vendor/github.com/gorilla/websocket/conn_write.go b/vendor/github.com/gorilla/websocket/conn_write.go new file mode 100644 index 0000000000..a509a21f87 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn_write.go @@ -0,0 +1,15 @@ +// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.8 + +package websocket + +import "net" + +func (c *Conn) writeBufs(bufs ...[]byte) error { + b := net.Buffers(bufs) + _, err := b.WriteTo(c.conn) + return err +} diff --git a/vendor/github.com/gorilla/websocket/conn_write_legacy.go b/vendor/github.com/gorilla/websocket/conn_write_legacy.go new file mode 100644 index 0000000000..37edaff5a5 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn_write_legacy.go @@ -0,0 +1,18 @@ +// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !go1.8 + +package websocket + +func (c *Conn) writeBufs(bufs ...[]byte) error { + for _, buf := range bufs { + if len(buf) > 0 { + if _, err := c.conn.Write(buf); err != nil { + return err + } + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/doc.go b/vendor/github.com/gorilla/websocket/doc.go new file mode 100644 index 0000000000..dcce1a63c0 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/doc.go @@ -0,0 +1,180 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package websocket implements the WebSocket protocol defined in RFC 6455. +// +// Overview +// +// The Conn type represents a WebSocket connection. A server application calls +// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn: +// +// var upgrader = websocket.Upgrader{ +// ReadBufferSize: 1024, +// WriteBufferSize: 1024, +// } +// +// func handler(w http.ResponseWriter, r *http.Request) { +// conn, err := upgrader.Upgrade(w, r, nil) +// if err != nil { +// log.Println(err) +// return +// } +// ... Use conn to send and receive messages. +// } +// +// Call the connection's WriteMessage and ReadMessage methods to send and +// receive messages as a slice of bytes. This snippet of code shows how to echo +// messages using these methods: +// +// for { +// messageType, p, err := conn.ReadMessage() +// if err != nil { +// log.Println(err) +// return +// } +// if err := conn.WriteMessage(messageType, p); err != nil { +// log.Println(err) +// return +// } +// } +// +// In above snippet of code, p is a []byte and messageType is an int with value +// websocket.BinaryMessage or websocket.TextMessage. +// +// An application can also send and receive messages using the io.WriteCloser +// and io.Reader interfaces. To send a message, call the connection NextWriter +// method to get an io.WriteCloser, write the message to the writer and close +// the writer when done. To receive a message, call the connection NextReader +// method to get an io.Reader and read until io.EOF is returned. This snippet +// shows how to echo messages using the NextWriter and NextReader methods: +// +// for { +// messageType, r, err := conn.NextReader() +// if err != nil { +// return +// } +// w, err := conn.NextWriter(messageType) +// if err != nil { +// return err +// } +// if _, err := io.Copy(w, r); err != nil { +// return err +// } +// if err := w.Close(); err != nil { +// return err +// } +// } +// +// Data Messages +// +// The WebSocket protocol distinguishes between text and binary data messages. +// Text messages are interpreted as UTF-8 encoded text. The interpretation of +// binary messages is left to the application. +// +// This package uses the TextMessage and BinaryMessage integer constants to +// identify the two data message types. The ReadMessage and NextReader methods +// return the type of the received message. The messageType argument to the +// WriteMessage and NextWriter methods specifies the type of a sent message. +// +// It is the application's responsibility to ensure that text messages are +// valid UTF-8 encoded text. +// +// Control Messages +// +// The WebSocket protocol defines three types of control messages: close, ping +// and pong. Call the connection WriteControl, WriteMessage or NextWriter +// methods to send a control message to the peer. +// +// Connections handle received close messages by calling the handler function +// set with the SetCloseHandler method and by returning a *CloseError from the +// NextReader, ReadMessage or the message Read method. The default close +// handler sends a close message to the peer. +// +// Connections handle received ping messages by calling the handler function +// set with the SetPingHandler method. The default ping handler sends a pong +// message to the peer. +// +// Connections handle received pong messages by calling the handler function +// set with the SetPongHandler method. The default pong handler does nothing. +// If an application sends ping messages, then the application should set a +// pong handler to receive the corresponding pong. +// +// The control message handler functions are called from the NextReader, +// ReadMessage and message reader Read methods. The default close and ping +// handlers can block these methods for a short time when the handler writes to +// the connection. +// +// The application must read the connection to process close, ping and pong +// messages sent from the peer. If the application is not otherwise interested +// in messages from the peer, then the application should start a goroutine to +// read and discard messages from the peer. A simple example is: +// +// func readLoop(c *websocket.Conn) { +// for { +// if _, _, err := c.NextReader(); err != nil { +// c.Close() +// break +// } +// } +// } +// +// Concurrency +// +// Connections support one concurrent reader and one concurrent writer. +// +// Applications are responsible for ensuring that no more than one goroutine +// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage, +// WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and +// that no more than one goroutine calls the read methods (NextReader, +// SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler) +// concurrently. +// +// The Close and WriteControl methods can be called concurrently with all other +// methods. +// +// Origin Considerations +// +// Web browsers allow Javascript applications to open a WebSocket connection to +// any host. It's up to the server to enforce an origin policy using the Origin +// request header sent by the browser. +// +// The Upgrader calls the function specified in the CheckOrigin field to check +// the origin. If the CheckOrigin function returns false, then the Upgrade +// method fails the WebSocket handshake with HTTP status 403. +// +// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail +// the handshake if the Origin request header is present and the Origin host is +// not equal to the Host request header. +// +// The deprecated package-level Upgrade function does not perform origin +// checking. The application is responsible for checking the Origin header +// before calling the Upgrade function. +// +// Compression EXPERIMENTAL +// +// Per message compression extensions (RFC 7692) are experimentally supported +// by this package in a limited capacity. Setting the EnableCompression option +// to true in Dialer or Upgrader will attempt to negotiate per message deflate +// support. +// +// var upgrader = websocket.Upgrader{ +// EnableCompression: true, +// } +// +// If compression was successfully negotiated with the connection's peer, any +// message received in compressed form will be automatically decompressed. +// All Read methods will return uncompressed bytes. +// +// Per message compression of messages written to a connection can be enabled +// or disabled by calling the corresponding Conn method: +// +// conn.EnableWriteCompression(false) +// +// Currently this package does not support compression with "context takeover". +// This means that messages must be compressed and decompressed in isolation, +// without retaining sliding window or dictionary state across messages. For +// more details refer to RFC 7692. +// +// Use of compression is experimental and may result in decreased performance. +package websocket diff --git a/vendor/github.com/gorilla/websocket/json.go b/vendor/github.com/gorilla/websocket/json.go new file mode 100644 index 0000000000..dc2c1f6415 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/json.go @@ -0,0 +1,60 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "encoding/json" + "io" +) + +// WriteJSON writes the JSON encoding of v as a message. +// +// Deprecated: Use c.WriteJSON instead. +func WriteJSON(c *Conn, v interface{}) error { + return c.WriteJSON(v) +} + +// WriteJSON writes the JSON encoding of v as a message. +// +// See the documentation for encoding/json Marshal for details about the +// conversion of Go values to JSON. +func (c *Conn) WriteJSON(v interface{}) error { + w, err := c.NextWriter(TextMessage) + if err != nil { + return err + } + err1 := json.NewEncoder(w).Encode(v) + err2 := w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// Deprecated: Use c.ReadJSON instead. +func ReadJSON(c *Conn, v interface{}) error { + return c.ReadJSON(v) +} + +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// See the documentation for the encoding/json Unmarshal function for details +// about the conversion of JSON to a Go value. +func (c *Conn) ReadJSON(v interface{}) error { + _, r, err := c.NextReader() + if err != nil { + return err + } + err = json.NewDecoder(r).Decode(v) + if err == io.EOF { + // One value is expected in the message. + err = io.ErrUnexpectedEOF + } + return err +} diff --git a/vendor/github.com/gorilla/websocket/mask.go b/vendor/github.com/gorilla/websocket/mask.go new file mode 100644 index 0000000000..577fce9efd --- /dev/null +++ b/vendor/github.com/gorilla/websocket/mask.go @@ -0,0 +1,54 @@ +// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in the +// LICENSE file. + +// +build !appengine + +package websocket + +import "unsafe" + +const wordSize = int(unsafe.Sizeof(uintptr(0))) + +func maskBytes(key [4]byte, pos int, b []byte) int { + // Mask one byte at a time for small buffers. + if len(b) < 2*wordSize { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 + } + + // Mask one byte at a time to word boundary. + if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 { + n = wordSize - n + for i := range b[:n] { + b[i] ^= key[pos&3] + pos++ + } + b = b[n:] + } + + // Create aligned word size key. + var k [wordSize]byte + for i := range k { + k[i] = key[(pos+i)&3] + } + kw := *(*uintptr)(unsafe.Pointer(&k)) + + // Mask one word at a time. + n := (len(b) / wordSize) * wordSize + for i := 0; i < n; i += wordSize { + *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw + } + + // Mask one byte at a time for remaining bytes. + b = b[n:] + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + + return pos & 3 +} diff --git a/vendor/github.com/gorilla/websocket/mask_safe.go b/vendor/github.com/gorilla/websocket/mask_safe.go new file mode 100644 index 0000000000..2aac060e52 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/mask_safe.go @@ -0,0 +1,15 @@ +// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of +// this source code is governed by a BSD-style license that can be found in the +// LICENSE file. + +// +build appengine + +package websocket + +func maskBytes(key [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 +} diff --git a/vendor/github.com/gorilla/websocket/prepared.go b/vendor/github.com/gorilla/websocket/prepared.go new file mode 100644 index 0000000000..74ec565d2c --- /dev/null +++ b/vendor/github.com/gorilla/websocket/prepared.go @@ -0,0 +1,102 @@ +// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bytes" + "net" + "sync" + "time" +) + +// PreparedMessage caches on the wire representations of a message payload. +// Use PreparedMessage to efficiently send a message payload to multiple +// connections. PreparedMessage is especially useful when compression is used +// because the CPU and memory expensive compression operation can be executed +// once for a given set of compression options. +type PreparedMessage struct { + messageType int + data []byte + mu sync.Mutex + frames map[prepareKey]*preparedFrame +} + +// prepareKey defines a unique set of options to cache prepared frames in PreparedMessage. +type prepareKey struct { + isServer bool + compress bool + compressionLevel int +} + +// preparedFrame contains data in wire representation. +type preparedFrame struct { + once sync.Once + data []byte +} + +// NewPreparedMessage returns an initialized PreparedMessage. You can then send +// it to connection using WritePreparedMessage method. Valid wire +// representation will be calculated lazily only once for a set of current +// connection options. +func NewPreparedMessage(messageType int, data []byte) (*PreparedMessage, error) { + pm := &PreparedMessage{ + messageType: messageType, + frames: make(map[prepareKey]*preparedFrame), + data: data, + } + + // Prepare a plain server frame. + _, frameData, err := pm.frame(prepareKey{isServer: true, compress: false}) + if err != nil { + return nil, err + } + + // To protect against caller modifying the data argument, remember the data + // copied to the plain server frame. + pm.data = frameData[len(frameData)-len(data):] + return pm, nil +} + +func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) { + pm.mu.Lock() + frame, ok := pm.frames[key] + if !ok { + frame = &preparedFrame{} + pm.frames[key] = frame + } + pm.mu.Unlock() + + var err error + frame.once.Do(func() { + // Prepare a frame using a 'fake' connection. + // TODO: Refactor code in conn.go to allow more direct construction of + // the frame. + mu := make(chan bool, 1) + mu <- true + var nc prepareConn + c := &Conn{ + conn: &nc, + mu: mu, + isServer: key.isServer, + compressionLevel: key.compressionLevel, + enableWriteCompression: true, + writeBuf: make([]byte, defaultWriteBufferSize+maxFrameHeaderSize), + } + if key.compress { + c.newCompressionWriter = compressNoContextTakeover + } + err = c.WriteMessage(pm.messageType, pm.data) + frame.data = nc.buf.Bytes() + }) + return pm.messageType, frame.data, err +} + +type prepareConn struct { + buf bytes.Buffer + net.Conn +} + +func (pc *prepareConn) Write(p []byte) (int, error) { return pc.buf.Write(p) } +func (pc *prepareConn) SetWriteDeadline(t time.Time) error { return nil } diff --git a/vendor/github.com/gorilla/websocket/proxy.go b/vendor/github.com/gorilla/websocket/proxy.go new file mode 100644 index 0000000000..bf2478e430 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/proxy.go @@ -0,0 +1,77 @@ +// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "encoding/base64" + "errors" + "net" + "net/http" + "net/url" + "strings" +) + +type netDialerFunc func(network, addr string) (net.Conn, error) + +func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) { + return fn(network, addr) +} + +func init() { + proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) { + return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil + }) +} + +type httpProxyDialer struct { + proxyURL *url.URL + fowardDial func(network, addr string) (net.Conn, error) +} + +func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) { + hostPort, _ := hostPortNoPort(hpd.proxyURL) + conn, err := hpd.fowardDial(network, hostPort) + if err != nil { + return nil, err + } + + connectHeader := make(http.Header) + if user := hpd.proxyURL.User; user != nil { + proxyUser := user.Username() + if proxyPassword, passwordSet := user.Password(); passwordSet { + credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword)) + connectHeader.Set("Proxy-Authorization", "Basic "+credential) + } + } + + connectReq := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: connectHeader, + } + + if err := connectReq.Write(conn); err != nil { + conn.Close() + return nil, err + } + + // Read response. It's OK to use and discard buffered reader here becaue + // the remote server does not speak until spoken to. + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + conn.Close() + return nil, err + } + + if resp.StatusCode != 200 { + conn.Close() + f := strings.SplitN(resp.Status, " ", 2) + return nil, errors.New(f[1]) + } + return conn, nil +} diff --git a/vendor/github.com/gorilla/websocket/server.go b/vendor/github.com/gorilla/websocket/server.go new file mode 100644 index 0000000000..a761824b33 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/server.go @@ -0,0 +1,363 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "errors" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +// HandshakeError describes an error with the handshake from the peer. +type HandshakeError struct { + message string +} + +func (e HandshakeError) Error() string { return e.message } + +// Upgrader specifies parameters for upgrading an HTTP connection to a +// WebSocket connection. +type Upgrader struct { + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer + // size is zero, then buffers allocated by the HTTP server are used. The + // I/O buffer sizes do not limit the size of the messages that can be sent + // or received. + ReadBufferSize, WriteBufferSize int + + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + + // Subprotocols specifies the server's supported protocols in order of + // preference. If this field is not nil, then the Upgrade method negotiates a + // subprotocol by selecting the first match in this list with a protocol + // requested by the client. If there's no match, then no protocol is + // negotiated (the Sec-Websocket-Protocol header is not included in the + // handshake response). + Subprotocols []string + + // Error specifies the function for generating HTTP error responses. If Error + // is nil, then http.Error is used to generate the HTTP response. + Error func(w http.ResponseWriter, r *http.Request, status int, reason error) + + // CheckOrigin returns true if the request Origin header is acceptable. If + // CheckOrigin is nil, then a safe default is used: return false if the + // Origin request header is present and the origin host is not equal to + // request Host header. + // + // A CheckOrigin function should carefully validate the request origin to + // prevent cross-site request forgery. + CheckOrigin func(r *http.Request) bool + + // EnableCompression specify if the server should attempt to negotiate per + // message compression (RFC 7692). Setting this value to true does not + // guarantee that compression will be supported. Currently only "no context + // takeover" modes are supported. + EnableCompression bool +} + +func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) { + err := HandshakeError{reason} + if u.Error != nil { + u.Error(w, r, status, err) + } else { + w.Header().Set("Sec-Websocket-Version", "13") + http.Error(w, http.StatusText(status), status) + } + return nil, err +} + +// checkSameOrigin returns true if the origin is not set or is equal to the request host. +func checkSameOrigin(r *http.Request) bool { + origin := r.Header["Origin"] + if len(origin) == 0 { + return true + } + u, err := url.Parse(origin[0]) + if err != nil { + return false + } + return equalASCIIFold(u.Host, r.Host) +} + +func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { + if u.Subprotocols != nil { + clientProtocols := Subprotocols(r) + for _, serverProtocol := range u.Subprotocols { + for _, clientProtocol := range clientProtocols { + if clientProtocol == serverProtocol { + return clientProtocol + } + } + } + } else if responseHeader != nil { + return responseHeader.Get("Sec-Websocket-Protocol") + } + return "" +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// application negotiated subprotocol (Sec-WebSocket-Protocol). +// +// If the upgrade fails, then Upgrade replies to the client with an HTTP error +// response. +func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) { + const badHandshake = "websocket: the client is not using the websocket protocol: " + + if !tokenListContainsValue(r.Header, "Connection", "upgrade") { + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header") + } + + if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header") + } + + if r.Method != "GET" { + return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET") + } + + if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header") + } + + if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported") + } + + checkOrigin := u.CheckOrigin + if checkOrigin == nil { + checkOrigin = checkSameOrigin + } + if !checkOrigin(r) { + return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin") + } + + challengeKey := r.Header.Get("Sec-Websocket-Key") + if challengeKey == "" { + return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-WebSocket-Key' header is missing or blank") + } + + subprotocol := u.selectSubprotocol(r, responseHeader) + + // Negotiate PMCE + var compress bool + if u.EnableCompression { + for _, ext := range parseExtensions(r.Header) { + if ext[""] != "permessage-deflate" { + continue + } + compress = true + break + } + } + + h, ok := w.(http.Hijacker) + if !ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") + } + var brw *bufio.ReadWriter + netConn, brw, err := h.Hijack() + if err != nil { + return u.returnError(w, r, http.StatusInternalServerError, err.Error()) + } + + if brw.Reader.Buffered() > 0 { + netConn.Close() + return nil, errors.New("websocket: client sent data before handshake is complete") + } + + var br *bufio.Reader + if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 { + // Reuse hijacked buffered reader as connection reader. + br = brw.Reader + } + + buf := bufioWriterBuffer(netConn, brw.Writer) + + var writeBuf []byte + if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 { + // Reuse hijacked write buffer as connection buffer. + writeBuf = buf + } + + c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf) + c.subprotocol = subprotocol + + if compress { + c.newCompressionWriter = compressNoContextTakeover + c.newDecompressionReader = decompressNoContextTakeover + } + + // Use larger of hijacked buffer and connection write buffer for header. + p := buf + if len(c.writeBuf) > len(p) { + p = c.writeBuf + } + p = p[:0] + + p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) + p = append(p, computeAcceptKey(challengeKey)...) + p = append(p, "\r\n"...) + if c.subprotocol != "" { + p = append(p, "Sec-WebSocket-Protocol: "...) + p = append(p, c.subprotocol...) + p = append(p, "\r\n"...) + } + if compress { + p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) + } + for k, vs := range responseHeader { + if k == "Sec-Websocket-Protocol" { + continue + } + for _, v := range vs { + p = append(p, k...) + p = append(p, ": "...) + for i := 0; i < len(v); i++ { + b := v[i] + if b <= 31 { + // prevent response splitting. + b = ' ' + } + p = append(p, b) + } + p = append(p, "\r\n"...) + } + } + p = append(p, "\r\n"...) + + // Clear deadlines set by HTTP server. + netConn.SetDeadline(time.Time{}) + + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout)) + } + if _, err = netConn.Write(p); err != nil { + netConn.Close() + return nil, err + } + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Time{}) + } + + return c, nil +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// Deprecated: Use websocket.Upgrader instead. +// +// Upgrade does not perform origin checking. The application is responsible for +// checking the Origin header before calling Upgrade. An example implementation +// of the same origin policy check is: +// +// if req.Header.Get("Origin") != "http://"+req.Host { +// http.Error(w, "Origin not allowed", http.StatusForbidden) +// return +// } +// +// If the endpoint supports subprotocols, then the application is responsible +// for negotiating the protocol used on the connection. Use the Subprotocols() +// function to get the subprotocols requested by the client. Use the +// Sec-Websocket-Protocol response header to specify the subprotocol selected +// by the application. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// negotiated subprotocol (Sec-Websocket-Protocol). +// +// The connection buffers IO to the underlying network connection. The +// readBufSize and writeBufSize parameters specify the size of the buffers to +// use. Messages can be larger than the buffers. +// +// If the request is not a valid WebSocket handshake, then Upgrade returns an +// error of type HandshakeError. Applications should handle this error by +// replying to the client with an HTTP error response. +func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) { + u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize} + u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) { + // don't return errors to maintain backwards compatibility + } + u.CheckOrigin = func(r *http.Request) bool { + // allow all connections by default + return true + } + return u.Upgrade(w, r, responseHeader) +} + +// Subprotocols returns the subprotocols requested by the client in the +// Sec-Websocket-Protocol header. +func Subprotocols(r *http.Request) []string { + h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol")) + if h == "" { + return nil + } + protocols := strings.Split(h, ",") + for i := range protocols { + protocols[i] = strings.TrimSpace(protocols[i]) + } + return protocols +} + +// IsWebSocketUpgrade returns true if the client requested upgrade to the +// WebSocket protocol. +func IsWebSocketUpgrade(r *http.Request) bool { + return tokenListContainsValue(r.Header, "Connection", "upgrade") && + tokenListContainsValue(r.Header, "Upgrade", "websocket") +} + +// bufioReaderSize size returns the size of a bufio.Reader. +func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int { + // This code assumes that peek on a reset reader returns + // bufio.Reader.buf[:0]. + // TODO: Use bufio.Reader.Size() after Go 1.10 + br.Reset(originalReader) + if p, err := br.Peek(0); err == nil { + return cap(p) + } + return 0 +} + +// writeHook is an io.Writer that records the last slice passed to it vio +// io.Writer.Write. +type writeHook struct { + p []byte +} + +func (wh *writeHook) Write(p []byte) (int, error) { + wh.p = p + return len(p), nil +} + +// bufioWriterBuffer grabs the buffer from a bufio.Writer. +func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte { + // This code assumes that bufio.Writer.buf[:1] is passed to the + // bufio.Writer's underlying writer. + var wh writeHook + bw.Reset(&wh) + bw.WriteByte(0) + bw.Flush() + + bw.Reset(originalWriter) + + return wh.p[:cap(wh.p)] +} diff --git a/vendor/github.com/gorilla/websocket/trace.go b/vendor/github.com/gorilla/websocket/trace.go new file mode 100644 index 0000000000..834f122a00 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/trace.go @@ -0,0 +1,19 @@ +// +build go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + if trace.TLSHandshakeStart != nil { + trace.TLSHandshakeStart() + } + err := doHandshake(tlsConn, cfg) + if trace.TLSHandshakeDone != nil { + trace.TLSHandshakeDone(tlsConn.ConnectionState(), err) + } + return err +} diff --git a/vendor/github.com/gorilla/websocket/trace_17.go b/vendor/github.com/gorilla/websocket/trace_17.go new file mode 100644 index 0000000000..77d05a0b57 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/trace_17.go @@ -0,0 +1,12 @@ +// +build !go1.8 + +package websocket + +import ( + "crypto/tls" + "net/http/httptrace" +) + +func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error { + return doHandshake(tlsConn, cfg) +} diff --git a/vendor/github.com/gorilla/websocket/util.go b/vendor/github.com/gorilla/websocket/util.go new file mode 100644 index 0000000000..354001e1ed --- /dev/null +++ b/vendor/github.com/gorilla/websocket/util.go @@ -0,0 +1,237 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "io" + "net/http" + "strings" + "unicode/utf8" +) + +var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +func computeAcceptKey(challengeKey string) string { + h := sha1.New() + h.Write([]byte(challengeKey)) + h.Write(keyGUID) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +func generateChallengeKey() (string, error) { + p := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, p); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(p), nil +} + +// Octet types from RFC 2616. +var octetTypes [256]byte + +const ( + isTokenOctet = 1 << iota + isSpaceOctet +) + +func init() { + // From RFC 2616 + // + // OCTET = + // CHAR = + // CTL = + // CR = + // LF = + // SP = + // HT = + // <"> = + // CRLF = CR LF + // LWS = [CRLF] 1*( SP | HT ) + // TEXT = + // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> + // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT + // token = 1* + // qdtext = > + + for c := 0; c < 256; c++ { + var t byte + isCtl := c <= 31 || c == 127 + isChar := 0 <= c && c <= 127 + isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0 + if strings.IndexRune(" \t\r\n", rune(c)) >= 0 { + t |= isSpaceOctet + } + if isChar && !isCtl && !isSeparator { + t |= isTokenOctet + } + octetTypes[c] = t + } +} + +func skipSpace(s string) (rest string) { + i := 0 + for ; i < len(s); i++ { + if octetTypes[s[i]]&isSpaceOctet == 0 { + break + } + } + return s[i:] +} + +func nextToken(s string) (token, rest string) { + i := 0 + for ; i < len(s); i++ { + if octetTypes[s[i]]&isTokenOctet == 0 { + break + } + } + return s[:i], s[i:] +} + +func nextTokenOrQuoted(s string) (value string, rest string) { + if !strings.HasPrefix(s, "\"") { + return nextToken(s) + } + s = s[1:] + for i := 0; i < len(s); i++ { + switch s[i] { + case '"': + return s[:i], s[i+1:] + case '\\': + p := make([]byte, len(s)-1) + j := copy(p, s[:i]) + escape := true + for i = i + 1; i < len(s); i++ { + b := s[i] + switch { + case escape: + escape = false + p[j] = b + j++ + case b == '\\': + escape = true + case b == '"': + return string(p[:j]), s[i+1:] + default: + p[j] = b + j++ + } + } + return "", "" + } + } + return "", "" +} + +// equalASCIIFold returns true if s is equal to t with ASCII case folding. +func equalASCIIFold(s, t string) bool { + for s != "" && t != "" { + sr, size := utf8.DecodeRuneInString(s) + s = s[size:] + tr, size := utf8.DecodeRuneInString(t) + t = t[size:] + if sr == tr { + continue + } + if 'A' <= sr && sr <= 'Z' { + sr = sr + 'a' - 'A' + } + if 'A' <= tr && tr <= 'Z' { + tr = tr + 'a' - 'A' + } + if sr != tr { + return false + } + } + return s == t +} + +// tokenListContainsValue returns true if the 1#token header with the given +// name contains a token equal to value with ASCII case folding. +func tokenListContainsValue(header http.Header, name string, value string) bool { +headers: + for _, s := range header[name] { + for { + var t string + t, s = nextToken(skipSpace(s)) + if t == "" { + continue headers + } + s = skipSpace(s) + if s != "" && s[0] != ',' { + continue headers + } + if equalASCIIFold(t, value) { + return true + } + if s == "" { + continue headers + } + s = s[1:] + } + } + return false +} + +// parseExtensions parses WebSocket extensions from a header. +func parseExtensions(header http.Header) []map[string]string { + // From RFC 6455: + // + // Sec-WebSocket-Extensions = extension-list + // extension-list = 1#extension + // extension = extension-token *( ";" extension-param ) + // extension-token = registered-token + // registered-token = token + // extension-param = token [ "=" (token | quoted-string) ] + // ;When using the quoted-string syntax variant, the value + // ;after quoted-string unescaping MUST conform to the + // ;'token' ABNF. + + var result []map[string]string +headers: + for _, s := range header["Sec-Websocket-Extensions"] { + for { + var t string + t, s = nextToken(skipSpace(s)) + if t == "" { + continue headers + } + ext := map[string]string{"": t} + for { + s = skipSpace(s) + if !strings.HasPrefix(s, ";") { + break + } + var k string + k, s = nextToken(skipSpace(s[1:])) + if k == "" { + continue headers + } + s = skipSpace(s) + var v string + if strings.HasPrefix(s, "=") { + v, s = nextTokenOrQuoted(skipSpace(s[1:])) + s = skipSpace(s) + } + if s != "" && s[0] != ',' && s[0] != ';' { + continue headers + } + ext[k] = v + } + if s != "" && s[0] != ',' { + continue headers + } + result = append(result, ext) + if s == "" { + continue headers + } + s = s[1:] + } + } + return result +} diff --git a/vendor/github.com/gorilla/websocket/x_net_proxy.go b/vendor/github.com/gorilla/websocket/x_net_proxy.go new file mode 100644 index 0000000000..2e668f6b88 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/x_net_proxy.go @@ -0,0 +1,473 @@ +// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. +//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy + +// Package proxy provides support for a variety of protocols to proxy network +// data. +// + +package websocket + +import ( + "errors" + "io" + "net" + "net/url" + "os" + "strconv" + "strings" + "sync" +) + +type proxy_direct struct{} + +// Direct is a direct proxy: one that makes network connections directly. +var proxy_Direct = proxy_direct{} + +func (proxy_direct) Dial(network, addr string) (net.Conn, error) { + return net.Dial(network, addr) +} + +// A PerHost directs connections to a default Dialer unless the host name +// requested matches one of a number of exceptions. +type proxy_PerHost struct { + def, bypass proxy_Dialer + + bypassNetworks []*net.IPNet + bypassIPs []net.IP + bypassZones []string + bypassHosts []string +} + +// NewPerHost returns a PerHost Dialer that directs connections to either +// defaultDialer or bypass, depending on whether the connection matches one of +// the configured rules. +func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost { + return &proxy_PerHost{ + def: defaultDialer, + bypass: bypass, + } +} + +// Dial connects to the address addr on the given network through either +// defaultDialer or bypass. +func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + return p.dialerForRequest(host).Dial(network, addr) +} + +func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer { + if ip := net.ParseIP(host); ip != nil { + for _, net := range p.bypassNetworks { + if net.Contains(ip) { + return p.bypass + } + } + for _, bypassIP := range p.bypassIPs { + if bypassIP.Equal(ip) { + return p.bypass + } + } + return p.def + } + + for _, zone := range p.bypassZones { + if strings.HasSuffix(host, zone) { + return p.bypass + } + if host == zone[1:] { + // For a zone ".example.com", we match "example.com" + // too. + return p.bypass + } + } + for _, bypassHost := range p.bypassHosts { + if bypassHost == host { + return p.bypass + } + } + return p.def +} + +// AddFromString parses a string that contains comma-separated values +// specifying hosts that should use the bypass proxy. Each value is either an +// IP address, a CIDR range, a zone (*.example.com) or a host name +// (localhost). A best effort is made to parse the string and errors are +// ignored. +func (p *proxy_PerHost) AddFromString(s string) { + hosts := strings.Split(s, ",") + for _, host := range hosts { + host = strings.TrimSpace(host) + if len(host) == 0 { + continue + } + if strings.Contains(host, "/") { + // We assume that it's a CIDR address like 127.0.0.0/8 + if _, net, err := net.ParseCIDR(host); err == nil { + p.AddNetwork(net) + } + continue + } + if ip := net.ParseIP(host); ip != nil { + p.AddIP(ip) + continue + } + if strings.HasPrefix(host, "*.") { + p.AddZone(host[1:]) + continue + } + p.AddHost(host) + } +} + +// AddIP specifies an IP address that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match an IP. +func (p *proxy_PerHost) AddIP(ip net.IP) { + p.bypassIPs = append(p.bypassIPs, ip) +} + +// AddNetwork specifies an IP range that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match. +func (p *proxy_PerHost) AddNetwork(net *net.IPNet) { + p.bypassNetworks = append(p.bypassNetworks, net) +} + +// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of +// "example.com" matches "example.com" and all of its subdomains. +func (p *proxy_PerHost) AddZone(zone string) { + if strings.HasSuffix(zone, ".") { + zone = zone[:len(zone)-1] + } + if !strings.HasPrefix(zone, ".") { + zone = "." + zone + } + p.bypassZones = append(p.bypassZones, zone) +} + +// AddHost specifies a host name that will use the bypass proxy. +func (p *proxy_PerHost) AddHost(host string) { + if strings.HasSuffix(host, ".") { + host = host[:len(host)-1] + } + p.bypassHosts = append(p.bypassHosts, host) +} + +// A Dialer is a means to establish a connection. +type proxy_Dialer interface { + // Dial connects to the given address via the proxy. + Dial(network, addr string) (c net.Conn, err error) +} + +// Auth contains authentication parameters that specific Dialers may require. +type proxy_Auth struct { + User, Password string +} + +// FromEnvironment returns the dialer specified by the proxy related variables in +// the environment. +func proxy_FromEnvironment() proxy_Dialer { + allProxy := proxy_allProxyEnv.Get() + if len(allProxy) == 0 { + return proxy_Direct + } + + proxyURL, err := url.Parse(allProxy) + if err != nil { + return proxy_Direct + } + proxy, err := proxy_FromURL(proxyURL, proxy_Direct) + if err != nil { + return proxy_Direct + } + + noProxy := proxy_noProxyEnv.Get() + if len(noProxy) == 0 { + return proxy + } + + perHost := proxy_NewPerHost(proxy, proxy_Direct) + perHost.AddFromString(noProxy) + return perHost +} + +// proxySchemes is a map from URL schemes to a function that creates a Dialer +// from a URL with such a scheme. +var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error) + +// RegisterDialerType takes a URL scheme and a function to generate Dialers from +// a URL with that scheme and a forwarding Dialer. Registered schemes are used +// by FromURL. +func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) { + if proxy_proxySchemes == nil { + proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) + } + proxy_proxySchemes[scheme] = f +} + +// FromURL returns a Dialer given a URL specification and an underlying +// Dialer for it to make network requests. +func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) { + var auth *proxy_Auth + if u.User != nil { + auth = new(proxy_Auth) + auth.User = u.User.Username() + if p, ok := u.User.Password(); ok { + auth.Password = p + } + } + + switch u.Scheme { + case "socks5": + return proxy_SOCKS5("tcp", u.Host, auth, forward) + } + + // If the scheme doesn't match any of the built-in schemes, see if it + // was registered by another package. + if proxy_proxySchemes != nil { + if f, ok := proxy_proxySchemes[u.Scheme]; ok { + return f(u, forward) + } + } + + return nil, errors.New("proxy: unknown scheme: " + u.Scheme) +} + +var ( + proxy_allProxyEnv = &proxy_envOnce{ + names: []string{"ALL_PROXY", "all_proxy"}, + } + proxy_noProxyEnv = &proxy_envOnce{ + names: []string{"NO_PROXY", "no_proxy"}, + } +) + +// envOnce looks up an environment variable (optionally by multiple +// names) once. It mitigates expensive lookups on some platforms +// (e.g. Windows). +// (Borrowed from net/http/transport.go) +type proxy_envOnce struct { + names []string + once sync.Once + val string +} + +func (e *proxy_envOnce) Get() string { + e.once.Do(e.init) + return e.val +} + +func (e *proxy_envOnce) init() { + for _, n := range e.names { + e.val = os.Getenv(n) + if e.val != "" { + return + } + } +} + +// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address +// with an optional username and password. See RFC 1928 and RFC 1929. +func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) { + s := &proxy_socks5{ + network: network, + addr: addr, + forward: forward, + } + if auth != nil { + s.user = auth.User + s.password = auth.Password + } + + return s, nil +} + +type proxy_socks5 struct { + user, password string + network, addr string + forward proxy_Dialer +} + +const proxy_socks5Version = 5 + +const ( + proxy_socks5AuthNone = 0 + proxy_socks5AuthPassword = 2 +) + +const proxy_socks5Connect = 1 + +const ( + proxy_socks5IP4 = 1 + proxy_socks5Domain = 3 + proxy_socks5IP6 = 4 +) + +var proxy_socks5Errors = []string{ + "", + "general failure", + "connection forbidden", + "network unreachable", + "host unreachable", + "connection refused", + "TTL expired", + "command not supported", + "address type not supported", +} + +// Dial connects to the address addr on the given network via the SOCKS5 proxy. +func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) { + switch network { + case "tcp", "tcp6", "tcp4": + default: + return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network) + } + + conn, err := s.forward.Dial(s.network, s.addr) + if err != nil { + return nil, err + } + if err := s.connect(conn, addr); err != nil { + conn.Close() + return nil, err + } + return conn, nil +} + +// connect takes an existing connection to a socks5 proxy server, +// and commands the server to extend that connection to target, +// which must be a canonical address with a host and port. +func (s *proxy_socks5) connect(conn net.Conn, target string) error { + host, portStr, err := net.SplitHostPort(target) + if err != nil { + return err + } + + port, err := strconv.Atoi(portStr) + if err != nil { + return errors.New("proxy: failed to parse port number: " + portStr) + } + if port < 1 || port > 0xffff { + return errors.New("proxy: port number out of range: " + portStr) + } + + // the size here is just an estimate + buf := make([]byte, 0, 6+len(host)) + + buf = append(buf, proxy_socks5Version) + if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 { + buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword) + } else { + buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone) + } + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + if buf[0] != 5 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0]))) + } + if buf[1] == 0xff { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication") + } + + // See RFC 1929 + if buf[1] == proxy_socks5AuthPassword { + buf = buf[:0] + buf = append(buf, 1 /* password protocol version */) + buf = append(buf, uint8(len(s.user))) + buf = append(buf, s.user...) + buf = append(buf, uint8(len(s.password))) + buf = append(buf, s.password...) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if buf[1] != 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password") + } + } + + buf = buf[:0] + buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */) + + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + buf = append(buf, proxy_socks5IP4) + ip = ip4 + } else { + buf = append(buf, proxy_socks5IP6) + } + buf = append(buf, ip...) + } else { + if len(host) > 255 { + return errors.New("proxy: destination host name too long: " + host) + } + buf = append(buf, proxy_socks5Domain) + buf = append(buf, byte(len(host))) + buf = append(buf, host...) + } + buf = append(buf, byte(port>>8), byte(port)) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:4]); err != nil { + return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + failure := "unknown error" + if int(buf[1]) < len(proxy_socks5Errors) { + failure = proxy_socks5Errors[buf[1]] + } + + if len(failure) > 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure) + } + + bytesToDiscard := 0 + switch buf[3] { + case proxy_socks5IP4: + bytesToDiscard = net.IPv4len + case proxy_socks5IP6: + bytesToDiscard = net.IPv6len + case proxy_socks5Domain: + _, err := io.ReadFull(conn, buf[:1]) + if err != nil { + return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + bytesToDiscard = int(buf[0]) + default: + return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr) + } + + if cap(buf) < bytesToDiscard { + buf = make([]byte, bytesToDiscard) + } else { + buf = buf[:bytesToDiscard] + } + if _, err := io.ReadFull(conn, buf); err != nil { + return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + // Also need to discard the port number + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + return nil +} diff --git a/vendor/github.com/kr/pty/.gitignore b/vendor/github.com/kr/pty/.gitignore new file mode 100644 index 0000000000..1f0a99f2f2 --- /dev/null +++ b/vendor/github.com/kr/pty/.gitignore @@ -0,0 +1,4 @@ +[568].out +_go* +_test* +_obj diff --git a/vendor/github.com/kr/pty/License b/vendor/github.com/kr/pty/License new file mode 100644 index 0000000000..6b7558b6b4 --- /dev/null +++ b/vendor/github.com/kr/pty/License @@ -0,0 +1,23 @@ +Copyright (c) 2011 Keith Rarick + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall +be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/kr/pty/README.md b/vendor/github.com/kr/pty/README.md new file mode 100644 index 0000000000..f9bb002e03 --- /dev/null +++ b/vendor/github.com/kr/pty/README.md @@ -0,0 +1,100 @@ +# pty + +Pty is a Go package for using unix pseudo-terminals. + +## Install + + go get github.com/kr/pty + +## Example + +### Command + +```go +package main + +import ( + "github.com/kr/pty" + "io" + "os" + "os/exec" +) + +func main() { + c := exec.Command("grep", "--color=auto", "bar") + f, err := pty.Start(c) + if err != nil { + panic(err) + } + + go func() { + f.Write([]byte("foo\n")) + f.Write([]byte("bar\n")) + f.Write([]byte("baz\n")) + f.Write([]byte{4}) // EOT + }() + io.Copy(os.Stdout, f) +} +``` + +### Shell + +```go +package main + +import ( + "io" + "log" + "os" + "os/exec" + "os/signal" + "syscall" + + "github.com/kr/pty" + "golang.org/x/crypto/ssh/terminal" +) + +func test() error { + // Create arbitrary command. + c := exec.Command("bash") + + // Start the command with a pty. + ptmx, err := pty.Start(c) + if err != nil { + return err + } + // Make sure to close the pty at the end. + defer func() { _ = ptmx.Close() }() // Best effort. + + // Handle pty size. + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGWINCH) + go func() { + for range ch { + if err := pty.InheritSize(os.Stdin, ptmx); err != nil { + log.Printf("error resizing pty: %s", err) + } + } + }() + ch <- syscall.SIGWINCH // Initial resize. + + // Set stdin in raw mode. + oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + panic(err) + } + defer func() { _ = terminal.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. + + // Copy stdin to the pty and the pty to stdout. + go func() { _, _ = io.Copy(ptmx, os.Stdin) }() + _, _ = io.Copy(os.Stdout, ptmx) + + return nil +} + +func main() { + if err := test(); err != nil { + log.Fatal(err) + } +} +``` diff --git a/vendor/github.com/kr/pty/doc.go b/vendor/github.com/kr/pty/doc.go new file mode 100644 index 0000000000..190cfbea92 --- /dev/null +++ b/vendor/github.com/kr/pty/doc.go @@ -0,0 +1,16 @@ +// Package pty provides functions for working with Unix terminals. +package pty + +import ( + "errors" + "os" +) + +// ErrUnsupported is returned if a function is not +// available on the current platform. +var ErrUnsupported = errors.New("unsupported") + +// Opens a pty and its corresponding tty. +func Open() (pty, tty *os.File, err error) { + return open() +} diff --git a/vendor/github.com/kr/pty/ioctl.go b/vendor/github.com/kr/pty/ioctl.go new file mode 100644 index 0000000000..c57c19e7e2 --- /dev/null +++ b/vendor/github.com/kr/pty/ioctl.go @@ -0,0 +1,13 @@ +// +build !windows + +package pty + +import "syscall" + +func ioctl(fd, cmd, ptr uintptr) error { + _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr) + if e != 0 { + return e + } + return nil +} diff --git a/vendor/github.com/kr/pty/ioctl_bsd.go b/vendor/github.com/kr/pty/ioctl_bsd.go new file mode 100644 index 0000000000..73b12c53cf --- /dev/null +++ b/vendor/github.com/kr/pty/ioctl_bsd.go @@ -0,0 +1,39 @@ +// +build darwin dragonfly freebsd netbsd openbsd + +package pty + +// from +const ( + _IOC_VOID uintptr = 0x20000000 + _IOC_OUT uintptr = 0x40000000 + _IOC_IN uintptr = 0x80000000 + _IOC_IN_OUT uintptr = _IOC_OUT | _IOC_IN + _IOC_DIRMASK = _IOC_VOID | _IOC_OUT | _IOC_IN + + _IOC_PARAM_SHIFT = 13 + _IOC_PARAM_MASK = (1 << _IOC_PARAM_SHIFT) - 1 +) + +func _IOC_PARM_LEN(ioctl uintptr) uintptr { + return (ioctl >> 16) & _IOC_PARAM_MASK +} + +func _IOC(inout uintptr, group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return inout | (param_len&_IOC_PARAM_MASK)<<16 | uintptr(group)<<8 | ioctl_num +} + +func _IO(group byte, ioctl_num uintptr) uintptr { + return _IOC(_IOC_VOID, group, ioctl_num, 0) +} + +func _IOR(group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return _IOC(_IOC_OUT, group, ioctl_num, param_len) +} + +func _IOW(group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return _IOC(_IOC_IN, group, ioctl_num, param_len) +} + +func _IOWR(group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return _IOC(_IOC_IN_OUT, group, ioctl_num, param_len) +} diff --git a/vendor/github.com/kr/pty/mktypes.bash b/vendor/github.com/kr/pty/mktypes.bash new file mode 100755 index 0000000000..82ee16721c --- /dev/null +++ b/vendor/github.com/kr/pty/mktypes.bash @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +GOOSARCH="${GOOS}_${GOARCH}" +case "$GOOSARCH" in +_* | *_ | _) + echo 'undefined $GOOS_$GOARCH:' "$GOOSARCH" 1>&2 + exit 1 + ;; +esac + +GODEFS="go tool cgo -godefs" + +$GODEFS types.go |gofmt > ztypes_$GOARCH.go + +case $GOOS in +freebsd|dragonfly|openbsd) + $GODEFS types_$GOOS.go |gofmt > ztypes_$GOOSARCH.go + ;; +esac diff --git a/vendor/github.com/kr/pty/pty_darwin.go b/vendor/github.com/kr/pty/pty_darwin.go new file mode 100644 index 0000000000..6344b6b0ef --- /dev/null +++ b/vendor/github.com/kr/pty/pty_darwin.go @@ -0,0 +1,65 @@ +package pty + +import ( + "errors" + "os" + "syscall" + "unsafe" +) + +func open() (pty, tty *os.File, err error) { + pFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_CLOEXEC, 0) + if err != nil { + return nil, nil, err + } + p := os.NewFile(uintptr(pFD), "/dev/ptmx") + // In case of error after this point, make sure we close the ptmx fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + if err := grantpt(p); err != nil { + return nil, nil, err + } + + if err := unlockpt(p); err != nil { + return nil, nil, err + } + + t, err := os.OpenFile(sname, os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func ptsname(f *os.File) (string, error) { + n := make([]byte, _IOC_PARM_LEN(syscall.TIOCPTYGNAME)) + + err := ioctl(f.Fd(), syscall.TIOCPTYGNAME, uintptr(unsafe.Pointer(&n[0]))) + if err != nil { + return "", err + } + + for i, c := range n { + if c == 0 { + return string(n[:i]), nil + } + } + return "", errors.New("TIOCPTYGNAME string not NUL-terminated") +} + +func grantpt(f *os.File) error { + return ioctl(f.Fd(), syscall.TIOCPTYGRANT, 0) +} + +func unlockpt(f *os.File) error { + return ioctl(f.Fd(), syscall.TIOCPTYUNLK, 0) +} diff --git a/vendor/github.com/kr/pty/pty_dragonfly.go b/vendor/github.com/kr/pty/pty_dragonfly.go new file mode 100644 index 0000000000..b7d1f20f29 --- /dev/null +++ b/vendor/github.com/kr/pty/pty_dragonfly.go @@ -0,0 +1,80 @@ +package pty + +import ( + "errors" + "os" + "strings" + "syscall" + "unsafe" +) + +// same code as pty_darwin.go +func open() (pty, tty *os.File, err error) { + p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + // In case of error after this point, make sure we close the ptmx fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + if err := grantpt(p); err != nil { + return nil, nil, err + } + + if err := unlockpt(p); err != nil { + return nil, nil, err + } + + t, err := os.OpenFile(sname, os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func grantpt(f *os.File) error { + _, err := isptmaster(f.Fd()) + return err +} + +func unlockpt(f *os.File) error { + _, err := isptmaster(f.Fd()) + return err +} + +func isptmaster(fd uintptr) (bool, error) { + err := ioctl(fd, syscall.TIOCISPTMASTER, 0) + return err == nil, err +} + +var ( + emptyFiodgnameArg fiodgnameArg + ioctl_FIODNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg)) +) + +func ptsname(f *os.File) (string, error) { + name := make([]byte, _C_SPECNAMELEN) + fa := fiodgnameArg{Name: (*byte)(unsafe.Pointer(&name[0])), Len: _C_SPECNAMELEN, Pad_cgo_0: [4]byte{0, 0, 0, 0}} + + err := ioctl(f.Fd(), ioctl_FIODNAME, uintptr(unsafe.Pointer(&fa))) + if err != nil { + return "", err + } + + for i, c := range name { + if c == 0 { + s := "/dev/" + string(name[:i]) + return strings.Replace(s, "ptm", "pts", -1), nil + } + } + return "", errors.New("TIOCPTYGNAME string not NUL-terminated") +} diff --git a/vendor/github.com/kr/pty/pty_freebsd.go b/vendor/github.com/kr/pty/pty_freebsd.go new file mode 100644 index 0000000000..63b6d91337 --- /dev/null +++ b/vendor/github.com/kr/pty/pty_freebsd.go @@ -0,0 +1,78 @@ +package pty + +import ( + "errors" + "os" + "syscall" + "unsafe" +) + +func posixOpenpt(oflag int) (fd int, err error) { + r0, _, e1 := syscall.Syscall(syscall.SYS_POSIX_OPENPT, uintptr(oflag), 0, 0) + fd = int(r0) + if e1 != 0 { + err = e1 + } + return fd, err +} + +func open() (pty, tty *os.File, err error) { + fd, err := posixOpenpt(syscall.O_RDWR | syscall.O_CLOEXEC) + if err != nil { + return nil, nil, err + } + p := os.NewFile(uintptr(fd), "/dev/pts") + // In case of error after this point, make sure we close the pts fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + t, err := os.OpenFile("/dev/"+sname, os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func isptmaster(fd uintptr) (bool, error) { + err := ioctl(fd, syscall.TIOCPTMASTER, 0) + return err == nil, err +} + +var ( + emptyFiodgnameArg fiodgnameArg + ioctlFIODGNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg)) +) + +func ptsname(f *os.File) (string, error) { + master, err := isptmaster(f.Fd()) + if err != nil { + return "", err + } + if !master { + return "", syscall.EINVAL + } + + const n = _C_SPECNAMELEN + 1 + var ( + buf = make([]byte, n) + arg = fiodgnameArg{Len: n, Buf: (*byte)(unsafe.Pointer(&buf[0]))} + ) + if err := ioctl(f.Fd(), ioctlFIODGNAME, uintptr(unsafe.Pointer(&arg))); err != nil { + return "", err + } + + for i, c := range buf { + if c == 0 { + return string(buf[:i]), nil + } + } + return "", errors.New("FIODGNAME string not NUL-terminated") +} diff --git a/vendor/github.com/kr/pty/pty_linux.go b/vendor/github.com/kr/pty/pty_linux.go new file mode 100644 index 0000000000..296dd21298 --- /dev/null +++ b/vendor/github.com/kr/pty/pty_linux.go @@ -0,0 +1,51 @@ +package pty + +import ( + "os" + "strconv" + "syscall" + "unsafe" +) + +func open() (pty, tty *os.File, err error) { + p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + // In case of error after this point, make sure we close the ptmx fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + if err := unlockpt(p); err != nil { + return nil, nil, err + } + + t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func ptsname(f *os.File) (string, error) { + var n _C_uint + err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))) + if err != nil { + return "", err + } + return "/dev/pts/" + strconv.Itoa(int(n)), nil +} + +func unlockpt(f *os.File) error { + var u _C_int + // use TIOCSPTLCK with a zero valued arg to clear the slave pty lock + return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))) +} diff --git a/vendor/github.com/kr/pty/pty_openbsd.go b/vendor/github.com/kr/pty/pty_openbsd.go new file mode 100644 index 0000000000..6e7aeae7c0 --- /dev/null +++ b/vendor/github.com/kr/pty/pty_openbsd.go @@ -0,0 +1,33 @@ +package pty + +import ( + "os" + "syscall" + "unsafe" +) + +func open() (pty, tty *os.File, err error) { + /* + * from ptm(4): + * The PTMGET command allocates a free pseudo terminal, changes its + * ownership to the caller, revokes the access privileges for all previous + * users, opens the file descriptors for the master and slave devices and + * returns them to the caller in struct ptmget. + */ + + p, err := os.OpenFile("/dev/ptm", os.O_RDWR|syscall.O_CLOEXEC, 0) + if err != nil { + return nil, nil, err + } + defer p.Close() + + var ptm ptmget + if err := ioctl(p.Fd(), uintptr(ioctl_PTMGET), uintptr(unsafe.Pointer(&ptm))); err != nil { + return nil, nil, err + } + + pty = os.NewFile(uintptr(ptm.Cfd), "/dev/ptm") + tty = os.NewFile(uintptr(ptm.Sfd), "/dev/ptm") + + return pty, tty, nil +} diff --git a/vendor/github.com/kr/pty/pty_unsupported.go b/vendor/github.com/kr/pty/pty_unsupported.go new file mode 100644 index 0000000000..9a3e721bc4 --- /dev/null +++ b/vendor/github.com/kr/pty/pty_unsupported.go @@ -0,0 +1,11 @@ +// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd + +package pty + +import ( + "os" +) + +func open() (pty, tty *os.File, err error) { + return nil, nil, ErrUnsupported +} diff --git a/vendor/github.com/kr/pty/run.go b/vendor/github.com/kr/pty/run.go new file mode 100644 index 0000000000..baecca8af9 --- /dev/null +++ b/vendor/github.com/kr/pty/run.go @@ -0,0 +1,34 @@ +// +build !windows + +package pty + +import ( + "os" + "os/exec" + "syscall" +) + +// Start assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout, +// and c.Stderr, calls c.Start, and returns the File of the tty's +// corresponding pty. +func Start(c *exec.Cmd) (pty *os.File, err error) { + pty, tty, err := Open() + if err != nil { + return nil, err + } + defer tty.Close() + c.Stdout = tty + c.Stdin = tty + c.Stderr = tty + if c.SysProcAttr == nil { + c.SysProcAttr = &syscall.SysProcAttr{} + } + c.SysProcAttr.Setctty = true + c.SysProcAttr.Setsid = true + err = c.Start() + if err != nil { + pty.Close() + return nil, err + } + return pty, err +} diff --git a/vendor/github.com/kr/pty/types.go b/vendor/github.com/kr/pty/types.go new file mode 100644 index 0000000000..5aecb6bcdc --- /dev/null +++ b/vendor/github.com/kr/pty/types.go @@ -0,0 +1,10 @@ +// +build ignore + +package pty + +import "C" + +type ( + _C_int C.int + _C_uint C.uint +) diff --git a/vendor/github.com/kr/pty/types_dragonfly.go b/vendor/github.com/kr/pty/types_dragonfly.go new file mode 100644 index 0000000000..5c0493b851 --- /dev/null +++ b/vendor/github.com/kr/pty/types_dragonfly.go @@ -0,0 +1,17 @@ +// +build ignore + +package pty + +/* +#define _KERNEL +#include +#include +#include +*/ +import "C" + +const ( + _C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */ +) + +type fiodgnameArg C.struct_fiodname_args diff --git a/vendor/github.com/kr/pty/types_freebsd.go b/vendor/github.com/kr/pty/types_freebsd.go new file mode 100644 index 0000000000..ce3eb95181 --- /dev/null +++ b/vendor/github.com/kr/pty/types_freebsd.go @@ -0,0 +1,15 @@ +// +build ignore + +package pty + +/* +#include +#include +*/ +import "C" + +const ( + _C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */ +) + +type fiodgnameArg C.struct_fiodgname_arg diff --git a/vendor/github.com/kr/pty/types_openbsd.go b/vendor/github.com/kr/pty/types_openbsd.go new file mode 100644 index 0000000000..47701b5f9e --- /dev/null +++ b/vendor/github.com/kr/pty/types_openbsd.go @@ -0,0 +1,14 @@ +// +build ignore + +package pty + +/* +#include +#include +#include +*/ +import "C" + +type ptmget C.struct_ptmget + +var ioctl_PTMGET = C.PTMGET diff --git a/vendor/github.com/kr/pty/util.go b/vendor/github.com/kr/pty/util.go new file mode 100644 index 0000000000..68a8584cfe --- /dev/null +++ b/vendor/github.com/kr/pty/util.go @@ -0,0 +1,64 @@ +// +build !windows + +package pty + +import ( + "os" + "syscall" + "unsafe" +) + +// InheritSize applies the terminal size of master to slave. This should be run +// in a signal handler for syscall.SIGWINCH to automatically resize the slave when +// the master receives a window size change notification. +func InheritSize(master, slave *os.File) error { + size, err := GetsizeFull(master) + if err != nil { + return err + } + err = Setsize(slave, size) + if err != nil { + return err + } + return nil +} + +// Setsize resizes t to s. +func Setsize(t *os.File, ws *Winsize) error { + return windowRectCall(ws, t.Fd(), syscall.TIOCSWINSZ) +} + +// GetsizeFull returns the full terminal size description. +func GetsizeFull(t *os.File) (size *Winsize, err error) { + var ws Winsize + err = windowRectCall(&ws, t.Fd(), syscall.TIOCGWINSZ) + return &ws, err +} + +// Getsize returns the number of rows (lines) and cols (positions +// in each line) in terminal t. +func Getsize(t *os.File) (rows, cols int, err error) { + ws, err := GetsizeFull(t) + return int(ws.Rows), int(ws.Cols), err +} + +// Winsize describes the terminal size. +type Winsize struct { + Rows uint16 // ws_row: Number of rows (in cells) + Cols uint16 // ws_col: Number of columns (in cells) + X uint16 // ws_xpixel: Width in pixels + Y uint16 // ws_ypixel: Height in pixels +} + +func windowRectCall(ws *Winsize, fd, a2 uintptr) error { + _, _, errno := syscall.Syscall( + syscall.SYS_IOCTL, + fd, + a2, + uintptr(unsafe.Pointer(ws)), + ) + if errno != 0 { + return syscall.Errno(errno) + } + return nil +} diff --git a/vendor/github.com/kr/pty/ztypes_386.go b/vendor/github.com/kr/pty/ztypes_386.go new file mode 100644 index 0000000000..ff0b8fd838 --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_386.go @@ -0,0 +1,9 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/kr/pty/ztypes_amd64.go b/vendor/github.com/kr/pty/ztypes_amd64.go new file mode 100644 index 0000000000..ff0b8fd838 --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_amd64.go @@ -0,0 +1,9 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/kr/pty/ztypes_arm.go b/vendor/github.com/kr/pty/ztypes_arm.go new file mode 100644 index 0000000000..ff0b8fd838 --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_arm.go @@ -0,0 +1,9 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/kr/pty/ztypes_arm64.go b/vendor/github.com/kr/pty/ztypes_arm64.go new file mode 100644 index 0000000000..6c29a4b918 --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_arm64.go @@ -0,0 +1,11 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +// +build arm64 + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/kr/pty/ztypes_dragonfly_amd64.go b/vendor/github.com/kr/pty/ztypes_dragonfly_amd64.go new file mode 100644 index 0000000000..6b0ba037f8 --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_dragonfly_amd64.go @@ -0,0 +1,14 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_dragonfly.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Name *byte + Len uint32 + Pad_cgo_0 [4]byte +} diff --git a/vendor/github.com/kr/pty/ztypes_freebsd_386.go b/vendor/github.com/kr/pty/ztypes_freebsd_386.go new file mode 100644 index 0000000000..d9975374e3 --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_freebsd_386.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Len int32 + Buf *byte +} diff --git a/vendor/github.com/kr/pty/ztypes_freebsd_amd64.go b/vendor/github.com/kr/pty/ztypes_freebsd_amd64.go new file mode 100644 index 0000000000..5fa102fcdf --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_freebsd_amd64.go @@ -0,0 +1,14 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Len int32 + Pad_cgo_0 [4]byte + Buf *byte +} diff --git a/vendor/github.com/kr/pty/ztypes_freebsd_arm.go b/vendor/github.com/kr/pty/ztypes_freebsd_arm.go new file mode 100644 index 0000000000..d9975374e3 --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_freebsd_arm.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Len int32 + Buf *byte +} diff --git a/vendor/github.com/kr/pty/ztypes_mipsx.go b/vendor/github.com/kr/pty/ztypes_mipsx.go new file mode 100644 index 0000000000..f0ce74086a --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_mipsx.go @@ -0,0 +1,12 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +// +build linux +// +build mips mipsle mips64 mips64le + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/kr/pty/ztypes_openbsd_386.go b/vendor/github.com/kr/pty/ztypes_openbsd_386.go new file mode 100644 index 0000000000..ccb3aab9ae --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_openbsd_386.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_openbsd.go + +package pty + +type ptmget struct { + Cfd int32 + Sfd int32 + Cn [16]int8 + Sn [16]int8 +} + +var ioctl_PTMGET = 0x40287401 diff --git a/vendor/github.com/kr/pty/ztypes_openbsd_amd64.go b/vendor/github.com/kr/pty/ztypes_openbsd_amd64.go new file mode 100644 index 0000000000..e67051688f --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_openbsd_amd64.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_openbsd.go + +package pty + +type ptmget struct { + Cfd int32 + Sfd int32 + Cn [16]int8 + Sn [16]int8 +} + +var ioctl_PTMGET = 0x40287401 diff --git a/vendor/github.com/kr/pty/ztypes_ppc64.go b/vendor/github.com/kr/pty/ztypes_ppc64.go new file mode 100644 index 0000000000..4e1af84312 --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_ppc64.go @@ -0,0 +1,11 @@ +// +build ppc64 + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/kr/pty/ztypes_ppc64le.go b/vendor/github.com/kr/pty/ztypes_ppc64le.go new file mode 100644 index 0000000000..e6780f4e23 --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_ppc64le.go @@ -0,0 +1,11 @@ +// +build ppc64le + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/kr/pty/ztypes_s390x.go b/vendor/github.com/kr/pty/ztypes_s390x.go new file mode 100644 index 0000000000..a7452b61cb --- /dev/null +++ b/vendor/github.com/kr/pty/ztypes_s390x.go @@ -0,0 +1,11 @@ +// +build s390x + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +)