Skip to content

Commit

Permalink
[supervisor] support tunneled ports
Browse files Browse the repository at this point in the history
  • Loading branch information
akosyakov committed May 20, 2021
1 parent 53e708f commit f7b2e37
Show file tree
Hide file tree
Showing 25 changed files with 3,504 additions and 374 deletions.
2 changes: 1 addition & 1 deletion components/gitpod-cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.7.0
github.com/spf13/cobra v0.0.5
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
google.golang.org/grpc v1.37.0
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20191105091915-95d230a53780 // indirect
Expand Down
4 changes: 4 additions & 0 deletions components/gitpod-cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down Expand Up @@ -273,6 +274,7 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down Expand Up @@ -318,6 +320,8 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
8 changes: 5 additions & 3 deletions components/gitpod-protocol/go/gitpod-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,10 @@ var errNotConnected = errors.New("not connected to Gitpod server")

// ConnectToServerOpts configures the server connection
type ConnectToServerOpts struct {
Context context.Context
Token string
Log *logrus.Entry
Context context.Context
Token string
Log *logrus.Entry
ReconnectionHandler func()
}

// ConnectToServer establishes a new websocket connection to the server
Expand Down Expand Up @@ -248,6 +249,7 @@ func ConnectToServer(endpoint string, opts ConnectToServerOpts) (*APIoverJSONRPC
reqHeader.Set("Authorization", "Bearer "+opts.Token)
}
ws := NewReconnectingWebsocket(endpoint, reqHeader, opts.Log)
ws.ReconnectionHandler = opts.ReconnectionHandler
go ws.Dial()

var res APIoverJSONRPC
Expand Down
113 changes: 77 additions & 36 deletions components/gitpod-protocol/go/reconnecting-ws.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
package protocol

import (
"context"
"encoding/json"
"errors"
"net/http"
"sync"
"time"

"github.com/gorilla/websocket"
Expand All @@ -23,11 +26,14 @@ type ReconnectingWebsocket struct {
maxReconnectionDelay time.Duration
reconnectionDelayGrowFactor float64

once sync.Once
closedCh chan struct{}
connCh chan chan *websocket.Conn
connCh chan chan *WebsocketConnection
errCh chan error

log *logrus.Entry

ReconnectionHandler func()
}

// NewReconnectingWebsocket creates a new instance of ReconnectingWebsocket
Expand All @@ -39,7 +45,7 @@ func NewReconnectingWebsocket(url string, reqHeader http.Header, log *logrus.Ent
maxReconnectionDelay: 30 * time.Second,
reconnectionDelayGrowFactor: 1.5,
handshakeTimeout: 2 * time.Second,
connCh: make(chan chan *websocket.Conn),
connCh: make(chan chan *WebsocketConnection),
closedCh: make(chan struct{}),
errCh: make(chan error),
log: log,
Expand All @@ -48,26 +54,27 @@ func NewReconnectingWebsocket(url string, reqHeader http.Header, log *logrus.Ent

// Close closes the underlying webscoket connection.
func (rc *ReconnectingWebsocket) Close() error {
close(rc.closedCh)
rc.once.Do(func() {
close(rc.closedCh)
})
return nil
}

// WriteObject 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 (rc *ReconnectingWebsocket) WriteObject(v interface{}) error {
// EnsureConnection ensures ws connections
// Returns only if connection is permanently failed
// If the passed handler returns false as closed then err is returned to the client,
// otherwise err is treated as a connection error, and new conneciton is provided.
func (rc *ReconnectingWebsocket) EnsureConnection(handler func(conn *WebsocketConnection) (closed bool, err error)) error {
for {
connCh := make(chan *websocket.Conn, 1)
connCh := make(chan *WebsocketConnection, 1)
select {
case <-rc.closedCh:
return errors.New("closed")
case rc.connCh <- connCh:
}
conn := <-connCh
err := conn.WriteJSON(v)
if err == nil {
return nil
}
if !websocket.IsUnexpectedCloseError(err) {
closed, err := handler(conn)
if !closed {
return err
}
select {
Expand All @@ -78,35 +85,62 @@ func (rc *ReconnectingWebsocket) WriteObject(v interface{}) error {
}
}

func isJSONError(err error) bool {
_, isJsonErr := err.(*json.InvalidUTF8Error)
if isJsonErr {
return true
}
_, isJsonErr = err.(*json.InvalidUnmarshalError)
if isJsonErr {
return true
}
_, isJsonErr = err.(*json.MarshalerError)
if isJsonErr {
return true
}
_, isJsonErr = err.(*json.SyntaxError)
if isJsonErr {
return true
}
_, isJsonErr = err.(*json.UnmarshalFieldError)
if isJsonErr {
return true
}
_, isJsonErr = err.(*json.UnmarshalTypeError)
if isJsonErr {
return true
}
_, isJsonErr = err.(*json.UnsupportedTypeError)
if isJsonErr {
return true
}
_, isJsonErr = err.(*json.UnsupportedValueError)
return isJsonErr
}

// WriteObject 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 (rc *ReconnectingWebsocket) WriteObject(v interface{}) error {
return rc.EnsureConnection(func(conn *WebsocketConnection) (bool, error) {
err := conn.WriteJSON(v)
closed := err != nil && !isJSONError(err)
return closed, err
})
}

// ReadObject 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 (rc *ReconnectingWebsocket) ReadObject(v interface{}) error {
for {
connCh := make(chan *websocket.Conn, 1)
select {
case <-rc.closedCh:
return errors.New("closed")
case rc.connCh <- connCh:
}
conn := <-connCh
return rc.EnsureConnection(func(conn *WebsocketConnection) (bool, error) {
err := conn.ReadJSON(v)
if err == nil {
return nil
}
if !websocket.IsUnexpectedCloseError(err) {
return err
}
select {
case <-rc.closedCh:
return errors.New("closed")
case rc.errCh <- err:
}
}
closed := err != nil && !isJSONError(err)
return closed, err
})
}

// Dial creates a new client connection.
func (rc *ReconnectingWebsocket) Dial() {
var conn *websocket.Conn
var conn *WebsocketConnection
defer func() {
if conn == nil {
return
Expand All @@ -129,19 +163,26 @@ func (rc *ReconnectingWebsocket) Dial() {

time.Sleep(1 * time.Second)
conn = rc.connect()
if conn != nil && rc.ReconnectionHandler != nil {
go rc.ReconnectionHandler()
}
}
}
}

func (rc *ReconnectingWebsocket) connect() *websocket.Conn {
func (rc *ReconnectingWebsocket) connect() *WebsocketConnection {
delay := rc.minReconnectionDelay
for {
dialer := websocket.Dialer{HandshakeTimeout: rc.handshakeTimeout}
conn, _, err := dialer.Dial(rc.url, rc.reqHeader)
if err == nil {
rc.log.WithField("url", rc.url).Info("connection was successfully established")

return conn
ws, err := NewWebsocketConnection(context.Background(), conn, func(staleErr error) {
rc.errCh <- staleErr
})
if err == nil {
return ws
}
}

rc.log.WithError(err).WithField("url", rc.url).Errorf("failed to connect, trying again in %d seconds...", uint32(delay.Seconds()))
Expand Down
139 changes: 139 additions & 0 deletions components/gitpod-protocol/go/websocket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.
/*---------------------------------------------------------------------------------------------
* Copyright (c) 2020 Jaime Pillora <[email protected]>. All rights reserved.
* Licensed under the MIT License. See https://github.com/jpillora/chisel/blob/7aa0da95db178b8bc4f20ab49128368348fd4410/LICENSE for license information.
*--------------------------------------------------------------------------------------------*/
// copied and modified from https://github.com/jpillora/chisel/blob/33fa2010abd42ec76ed9011995f5240642b1a3c5/share/cnet/conn_ws.go
package protocol

import (
"context"
"sync"
"time"

"github.com/gorilla/websocket"
)

type WebsocketConnection struct {
*websocket.Conn
buff []byte

Ctx context.Context
cancel func()

once sync.Once
closeErr error
waitDone chan struct{}
}

var (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second

// Time allowed to read the next pong message from the peer.
pongWait = 15 * time.Second

// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
)

//NewWebsocketConnection converts a websocket.Conn into a net.Conn
func NewWebsocketConnection(ctx context.Context, websocketConn *websocket.Conn, onStale func(staleErr error)) (*WebsocketConnection, error) {
ctx, cancel := context.WithCancel(ctx)
c := &WebsocketConnection{
Conn: websocketConn,
waitDone: make(chan struct{}),
Ctx: ctx,
cancel: cancel,
}
err := c.SetReadDeadline(time.Now().Add(pongWait))
if err != nil {
return nil, err
}
c.SetPongHandler(func(string) error { c.SetReadDeadline(time.Now().Add(pongWait)); return nil })

go func() {
defer c.Close()
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
staleErr := c.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait))
if staleErr != nil {
onStale(staleErr)
return
}
}
}
}()
return c, nil
}

// Close closes the connection
func (c *WebsocketConnection) Close() error {
c.once.Do(func() {
c.cancel()
c.closeErr = c.Conn.Close()
close(c.waitDone)
})
return c.closeErr
}

// Wait waits till the connection is closed.
func (c *WebsocketConnection) Wait() error {
<-c.waitDone
return c.closeErr
}

//Read is not threadsafe though thats okay since there
//should never be more than one reader
func (c *WebsocketConnection) Read(dst []byte) (int, error) {
ldst := len(dst)
//use buffer or read new message
var src []byte
if len(c.buff) > 0 {
src = c.buff
c.buff = nil
} else if _, msg, err := c.Conn.ReadMessage(); err == nil {
src = msg
} else {
return 0, err
}
//copy src->dest
var n int
if len(src) > ldst {
//copy as much as possible of src into dst
n = copy(dst, src[:ldst])
//copy remainder into buffer
r := src[ldst:]
lr := len(r)
c.buff = make([]byte, lr)
copy(c.buff, r)
} else {
//copy all of src into dst
n = copy(dst, src)
}
//return bytes copied
return n, nil
}

func (c *WebsocketConnection) Write(b []byte) (int, error) {
err := c.Conn.WriteMessage(websocket.BinaryMessage, b)
if err != nil {
return 0, err
}
n := len(b)
return n, nil
}

func (c *WebsocketConnection) SetDeadline(t time.Time) error {
if err := c.Conn.SetReadDeadline(t); err != nil {
return err
}
return c.Conn.SetWriteDeadline(t)
}
Loading

0 comments on commit f7b2e37

Please sign in to comment.