From 6780acc5fd3e2a1b3c1afd0d787fe83bd444665a Mon Sep 17 00:00:00 2001 From: Zexi Li Date: Tue, 19 Feb 2019 11:41:41 +0800 Subject: [PATCH] webconsole: support websocket proxy --- Gopkg.lock | 10 + Gopkg.toml | 4 + pkg/compute/models/hoststorages.go | 2 +- pkg/webconsole/handlers.go | 7 +- pkg/webconsole/server/server.go | 4 +- .../server/websocketproxy_server.go | 50 ++++ pkg/webconsole/service/service.go | 8 +- .../koding/websocketproxy/.travis.yml | 2 + .../koding/websocketproxy/LICENSE.md | 20 ++ .../koding/websocketproxy/README.md | 54 ++++ .../koding/websocketproxy/websocketproxy.go | 233 ++++++++++++++++++ 11 files changed, 387 insertions(+), 7 deletions(-) create mode 100644 pkg/webconsole/server/websocketproxy_server.go create mode 100644 vendor/github.com/koding/websocketproxy/.travis.yml create mode 100644 vendor/github.com/koding/websocketproxy/LICENSE.md create mode 100644 vendor/github.com/koding/websocketproxy/README.md create mode 100644 vendor/github.com/koding/websocketproxy/websocketproxy.go diff --git a/Gopkg.lock b/Gopkg.lock index 7e20fb308d..47524fbe11 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -800,6 +800,14 @@ revision = "1624edc4454b8682399def8740d46db5e4362ba4" version = "v1.1.5" +[[projects]] + branch = "master" + digest = "1:81abe06bccf5e35235e8d6387508a4f3417393216e62bd4c2e383bbebd4b4fbb" + name = "github.com/koding/websocketproxy" + packages = ["."] + pruneopts = "UT" + revision = "7ed82d81a28c9ba1ed6fd1157ce714760f214c98" + [[projects]] digest = "1:0a69a1c0db3591fcefb47f115b224592c8dfa4368b7ba9fae509d5e16cdc95c8" name = "github.com/konsorten/go-windows-terminal-sequences" @@ -1749,6 +1757,7 @@ "github.com/anacrolix/torrent", "github.com/anacrolix/torrent/bencode", "github.com/anacrolix/torrent/metainfo", + "github.com/anacrolix/torrent/storage", "github.com/aokoli/goutils", "github.com/aws/aws-sdk-go/aws", "github.com/aws/aws-sdk-go/aws/credentials", @@ -1794,6 +1803,7 @@ "github.com/jinzhu/gorm", "github.com/jinzhu/gorm/dialects/mysql", "github.com/json-iterator/go", + "github.com/koding/websocketproxy", "github.com/kr/pty", "github.com/mholt/caddy", "github.com/mholt/caddy/startupshutdown", diff --git a/Gopkg.toml b/Gopkg.toml index 2a76ddc2e7..5c1fc99166 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -92,6 +92,10 @@ name = "github.com/gorilla/websocket" version = "1.4.0" +[[constraint]] + name = "github.com/koding/websocketproxy" + branch = "master" + [[constraint]] branch = "master" name = "github.com/gosuri/uitable" diff --git a/pkg/compute/models/hoststorages.go b/pkg/compute/models/hoststorages.go index a42ccd736a..f80c6935e9 100644 --- a/pkg/compute/models/hoststorages.go +++ b/pkg/compute/models/hoststorages.go @@ -190,7 +190,7 @@ func (self *SHoststorage) getExtraDetails(extra *jsonutils.JSONDict) *jsonutils. wasted := storage.GetUsedCapacity(tristate.False) extra.Add(jsonutils.NewInt(int64(used)), "used_capacity") extra.Add(jsonutils.NewInt(int64(wasted)), "waste_capacity") - extra.Add(jsonutils.NewInt(int64(storage.Capacity-used-wasted)), "free_capacity") + extra.Add(jsonutils.NewInt(int64(storage.GetFreeCapacity())), "free_capacity") extra.Add(jsonutils.NewString(storage.StorageType), "storage_type") extra.Add(jsonutils.NewString(storage.MediumType), "medium_type") extra.Add(jsonutils.NewBool(storage.Enabled), "enabled") diff --git a/pkg/webconsole/handlers.go b/pkg/webconsole/handlers.go index 8af86d3181..06f9a2ac5d 100644 --- a/pkg/webconsole/handlers.go +++ b/pkg/webconsole/handlers.go @@ -22,9 +22,10 @@ import ( ) const ( - ApiPathPrefix = "/webconsole/" - ConnectPathPrefix = "/connect/" - WebsockifyPathPrefix = "/websockify/" + ApiPathPrefix = "/webconsole/" + ConnectPathPrefix = "/connect/" + WebsockifyPathPrefix = "/websockify/" + WebsocketProxyPathPrefix = "/wsproxy/" ) func InitHandlers(app *appsrv.Application) { diff --git a/pkg/webconsole/server/server.go b/pkg/webconsole/server/server.go index 7028528aba..fa382803e0 100644 --- a/pkg/webconsole/server/server.go +++ b/pkg/webconsole/server/server.go @@ -37,8 +37,10 @@ func (s *ConnectionServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { var srv http.Handler protocol := sessionObj.GetProtocol() switch protocol { - case session.VNC, session.SPICE, session.WMKS: + case session.VNC, session.SPICE: srv, err = NewWebsockifyServer(sessionObj) + case session.WMKS: + srv, err = NewWebsocketProxyServer(sessionObj) default: srv, err = NewTTYServer(sessionObj) } diff --git a/pkg/webconsole/server/websocketproxy_server.go b/pkg/webconsole/server/websocketproxy_server.go new file mode 100644 index 0000000000..0e6fff4654 --- /dev/null +++ b/pkg/webconsole/server/websocketproxy_server.go @@ -0,0 +1,50 @@ +package server + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/gorilla/websocket" + "github.com/koding/websocketproxy" + + "yunion.io/x/onecloud/pkg/webconsole/session" +) + +type WebsocketProxyServer struct { + Session *session.SSession + proxy *websocketproxy.WebsocketProxy +} + +func NewWebsocketProxyServer(s *session.SSession) (*WebsocketProxyServer, error) { + info := s.ISessionData.(*session.RemoteConsoleInfo) + if info.Url == "" { + return nil, fmt.Errorf("Empty proxy url") + } + u, err := url.Parse(info.Url) + if err != nil { + return nil, fmt.Errorf("Parse url %s: %v", info.Url, err) + } + proxySrv := websocketproxy.NewProxy(u) + proxySrv.Dialer = &websocket.Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + proxySrv.Backend = func(_ *http.Request) *url.URL { + return u + } + proxySrv.Upgrader = &upgrader + return &WebsocketProxyServer{ + Session: s, + proxy: proxySrv, + }, nil +} + +func (s *WebsocketProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.proxy.ServeHTTP(w, r) +} diff --git a/pkg/webconsole/service/service.go b/pkg/webconsole/service/service.go index 7cfbfb14d3..18be70cd1a 100644 --- a/pkg/webconsole/service/service.go +++ b/pkg/webconsole/service/service.go @@ -58,11 +58,15 @@ func start() { // api handler root.PathPrefix(webconsole.ApiPathPrefix).Handler(app) + srv := server.NewConnectionServer() // websocket command text console handler - root.Handle(webconsole.ConnectPathPrefix, server.NewConnectionServer()) + root.Handle(webconsole.ConnectPathPrefix, srv) // websockify graphic console handler - root.Handle(webconsole.WebsockifyPathPrefix, server.NewConnectionServer()) + root.Handle(webconsole.WebsockifyPathPrefix, srv) + + // websocketproxy handler + root.Handle(webconsole.WebsocketProxyPathPrefix, srv) addr := net.JoinHostPort(o.Options.Address, strconv.Itoa(o.Options.Port)) log.Infof("Start listen on %s", addr) diff --git a/vendor/github.com/koding/websocketproxy/.travis.yml b/vendor/github.com/koding/websocketproxy/.travis.yml new file mode 100644 index 0000000000..8670f006c2 --- /dev/null +++ b/vendor/github.com/koding/websocketproxy/.travis.yml @@ -0,0 +1,2 @@ +language: go +go: 1.8 diff --git a/vendor/github.com/koding/websocketproxy/LICENSE.md b/vendor/github.com/koding/websocketproxy/LICENSE.md new file mode 100644 index 0000000000..f0a2a7c7f2 --- /dev/null +++ b/vendor/github.com/koding/websocketproxy/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Koding, Inc. + +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/koding/websocketproxy/README.md b/vendor/github.com/koding/websocketproxy/README.md new file mode 100644 index 0000000000..526bb43f86 --- /dev/null +++ b/vendor/github.com/koding/websocketproxy/README.md @@ -0,0 +1,54 @@ +# WebsocketProxy [![GoDoc](https://godoc.org/github.com/koding/websocketproxy?status.svg)](https://godoc.org/github.com/koding/websocketproxy) [![Build Status](https://travis-ci.org/koding/websocketproxy.svg)](https://travis-ci.org/koding/websocketproxy) + +WebsocketProxy is an http.Handler interface build on top of +[gorilla/websocket](https://github.com/gorilla/websocket) that you can plug +into your existing Go webserver to provide WebSocket reverse proxy. + +## Install + +```bash +go get github.com/koding/websocketproxy +``` + +## Example + +Below is a simple server that proxies to the given backend URL + +```go +package main + +import ( + "flag" + "net/http" + "net/url" + + "github.com/koding/websocketproxy" +) + +var ( + flagBackend = flag.String("backend", "", "Backend URL for proxying") +) + +func main() { + u, err := url.Parse(*flagBackend) + if err != nil { + log.Fatalln(err) + } + + err = http.ListenAndServe(":80", websocketproxy.NewProxy(u)) + if err != nil { + log.Fatalln(err) + } +} +``` + +Save it as `proxy.go` and run as: + +```bash +go run proxy.go -backend ws://example.com:3000 +``` + +Now all incoming WebSocket requests coming to this server will be proxied to +`ws://example.com:3000` + + diff --git a/vendor/github.com/koding/websocketproxy/websocketproxy.go b/vendor/github.com/koding/websocketproxy/websocketproxy.go new file mode 100644 index 0000000000..63d39ba95e --- /dev/null +++ b/vendor/github.com/koding/websocketproxy/websocketproxy.go @@ -0,0 +1,233 @@ +// Package websocketproxy is a reverse proxy for WebSocket connections. +package websocketproxy + +import ( + "fmt" + "io" + "log" + "net" + "net/http" + "net/url" + "strings" + + "github.com/gorilla/websocket" +) + +var ( + // DefaultUpgrader specifies the parameters for upgrading an HTTP + // connection to a WebSocket connection. + DefaultUpgrader = &websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } + + // DefaultDialer is a dialer with all fields set to the default zero values. + DefaultDialer = websocket.DefaultDialer +) + +// WebsocketProxy is an HTTP Handler that takes an incoming WebSocket +// connection and proxies it to another server. +type WebsocketProxy struct { + // Director, if non-nil, is a function that may copy additional request + // headers from the incoming WebSocket connection into the output headers + // which will be forwarded to another server. + Director func(incoming *http.Request, out http.Header) + + // Backend returns the backend URL which the proxy uses to reverse proxy + // the incoming WebSocket connection. Request is the initial incoming and + // unmodified request. + Backend func(*http.Request) *url.URL + + // Upgrader specifies the parameters for upgrading a incoming HTTP + // connection to a WebSocket connection. If nil, DefaultUpgrader is used. + Upgrader *websocket.Upgrader + + // Dialer contains options for connecting to the backend WebSocket server. + // If nil, DefaultDialer is used. + Dialer *websocket.Dialer +} + +// ProxyHandler returns a new http.Handler interface that reverse proxies the +// request to the given target. +func ProxyHandler(target *url.URL) http.Handler { return NewProxy(target) } + +// NewProxy returns a new Websocket reverse proxy that rewrites the +// URL's to the scheme, host and base path provider in target. +func NewProxy(target *url.URL) *WebsocketProxy { + backend := func(r *http.Request) *url.URL { + // Shallow copy + u := *target + u.Fragment = r.URL.Fragment + u.Path = r.URL.Path + u.RawQuery = r.URL.RawQuery + return &u + } + return &WebsocketProxy{Backend: backend} +} + +// ServeHTTP implements the http.Handler that proxies WebSocket connections. +func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if w.Backend == nil { + log.Println("websocketproxy: backend function is not defined") + http.Error(rw, "internal server error (code: 1)", http.StatusInternalServerError) + return + } + + backendURL := w.Backend(req) + if backendURL == nil { + log.Println("websocketproxy: backend URL is nil") + http.Error(rw, "internal server error (code: 2)", http.StatusInternalServerError) + return + } + + dialer := w.Dialer + if w.Dialer == nil { + dialer = DefaultDialer + } + + // Pass headers from the incoming request to the dialer to forward them to + // the final destinations. + requestHeader := http.Header{} + if origin := req.Header.Get("Origin"); origin != "" { + requestHeader.Add("Origin", origin) + } + for _, prot := range req.Header[http.CanonicalHeaderKey("Sec-WebSocket-Protocol")] { + requestHeader.Add("Sec-WebSocket-Protocol", prot) + } + for _, cookie := range req.Header[http.CanonicalHeaderKey("Cookie")] { + requestHeader.Add("Cookie", cookie) + } + if req.Host != "" { + requestHeader.Set("Host", req.Host) + } + + // Pass X-Forwarded-For headers too, code below is a part of + // httputil.ReverseProxy. See http://en.wikipedia.org/wiki/X-Forwarded-For + // for more information + // TODO: use RFC7239 http://tools.ietf.org/html/rfc7239 + if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { + // If we aren't the first proxy retain prior + // X-Forwarded-For information as a comma+space + // separated list and fold multiple headers into one. + if prior, ok := req.Header["X-Forwarded-For"]; ok { + clientIP = strings.Join(prior, ", ") + ", " + clientIP + } + requestHeader.Set("X-Forwarded-For", clientIP) + } + + // Set the originating protocol of the incoming HTTP request. The SSL might + // be terminated on our site and because we doing proxy adding this would + // be helpful for applications on the backend. + requestHeader.Set("X-Forwarded-Proto", "http") + if req.TLS != nil { + requestHeader.Set("X-Forwarded-Proto", "https") + } + + // Enable the director to copy any additional headers it desires for + // forwarding to the remote server. + if w.Director != nil { + w.Director(req, requestHeader) + } + + // Connect to the backend URL, also pass the headers we get from the requst + // together with the Forwarded headers we prepared above. + // TODO: support multiplexing on the same backend connection instead of + // opening a new TCP connection time for each request. This should be + // optional: + // http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01 + connBackend, resp, err := dialer.Dial(backendURL.String(), requestHeader) + if err != nil { + log.Printf("websocketproxy: couldn't dial to remote backend url %s", err) + if resp != nil { + // If the WebSocket handshake fails, ErrBadHandshake is returned + // along with a non-nil *http.Response so that callers can handle + // redirects, authentication, etcetera. + if err := copyResponse(rw, resp); err != nil { + log.Printf("websocketproxy: couldn't write response after failed remote backend handshake: %s", err) + } + } else { + http.Error(rw, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) + } + return + } + defer connBackend.Close() + + upgrader := w.Upgrader + if w.Upgrader == nil { + upgrader = DefaultUpgrader + } + + // Only pass those headers to the upgrader. + upgradeHeader := http.Header{} + if hdr := resp.Header.Get("Sec-Websocket-Protocol"); hdr != "" { + upgradeHeader.Set("Sec-Websocket-Protocol", hdr) + } + if hdr := resp.Header.Get("Set-Cookie"); hdr != "" { + upgradeHeader.Set("Set-Cookie", hdr) + } + + // Now upgrade the existing incoming request to a WebSocket connection. + // Also pass the header that we gathered from the Dial handshake. + connPub, err := upgrader.Upgrade(rw, req, upgradeHeader) + if err != nil { + log.Printf("websocketproxy: couldn't upgrade %s", err) + return + } + defer connPub.Close() + + errClient := make(chan error, 1) + errBackend := make(chan error, 1) + replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) { + for { + msgType, msg, err := src.ReadMessage() + if err != nil { + m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) + if e, ok := err.(*websocket.CloseError); ok { + if e.Code != websocket.CloseNoStatusReceived { + m = websocket.FormatCloseMessage(e.Code, e.Text) + } + } + errc <- err + dst.WriteMessage(websocket.CloseMessage, m) + break + } + err = dst.WriteMessage(msgType, msg) + if err != nil { + errc <- err + break + } + } + } + + go replicateWebsocketConn(connPub, connBackend, errClient) + go replicateWebsocketConn(connBackend, connPub, errBackend) + + var message string + select { + case err = <-errClient: + message = "websocketproxy: Error when copying from backend to client: %v" + case err = <-errBackend: + message = "websocketproxy: Error when copying from client to backend: %v" + + } + if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure { + log.Printf(message, err) + } +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +func copyResponse(rw http.ResponseWriter, resp *http.Response) error { + copyHeader(rw.Header(), resp.Header) + rw.WriteHeader(resp.StatusCode) + defer resp.Body.Close() + + _, err := io.Copy(rw, resp.Body) + return err +}