From 0a70ff49c5856de882932cf74d8869206a3169df Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Tue, 14 May 2024 15:36:25 +0200 Subject: [PATCH 1/8] feat: add remote access client to proxy local tcp/socket or stdio connections to the remote websocket --- pkg/proxy/proxy.go | 98 ++++++++++++++++++ pkg/remoteaccess/client.go | 153 +++++++++++++++++++++++++++++ pkg/wsconnadapter/wsconnadapter.go | 102 +++++++++++++++++++ 3 files changed, 353 insertions(+) create mode 100644 pkg/proxy/proxy.go create mode 100644 pkg/remoteaccess/client.go create mode 100644 pkg/wsconnadapter/wsconnadapter.go diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go new file mode 100644 index 00000000..3b7eea4f --- /dev/null +++ b/pkg/proxy/proxy.go @@ -0,0 +1,98 @@ +package proxy + +import ( + "io" + "net" + + "github.com/gorilla/websocket" + "github.com/reubenmiller/go-c8y/pkg/wsconnadapter" + + "github.com/reubenmiller/go-c8y/pkg/logger" +) + +var Logger logger.Logger + +func init() { + Logger = logger.NewLogger("proxy") +} + +func chanFromConn(conn io.Reader) chan []byte { + c := make(chan []byte) + + go func() { + b := make([]byte, 1024) + + for { + n, err := conn.Read(b) + if n > 0 { + res := make([]byte, n) + // Copy the buffer so it doesn't get changed while read by the recipient. + copy(res, b[:n]) + c <- res + } + if err != nil { + c <- nil + break + } + } + }() + + return c +} + +// Copy accepts a websocket connection and TCP connection and copies data between them +func Copy(gwsConn *websocket.Conn, tcpConn net.Conn) { + wsConn := wsconnadapter.New(gwsConn) + wsChan := chanFromConn(wsConn) + tcpChan := chanFromConn(tcpConn) + + defer wsConn.Close() + defer tcpConn.Close() + for { + select { + case wsData := <-wsChan: + if wsData == nil { + Logger.Infof("Connection closed: D: %v, S: %v", tcpConn.LocalAddr(), wsConn.RemoteAddr()) + return + } else { + tcpConn.Write(wsData) + } + case tcpData := <-tcpChan: + if tcpData == nil { + Logger.Infof("Connection closed: D: %v, S: %v", tcpConn.LocalAddr(), wsConn.LocalAddr()) + return + } else { + wsConn.Write(tcpData) + } + } + } + +} + +// Copy accepts a websocket connection and read/writer and copies data between them +func CopyReadWriter(gwsConn *websocket.Conn, r io.ReadCloser, w io.Writer) { + wsConn := wsconnadapter.New(gwsConn) + wsChan := chanFromConn(wsConn) + stdioChan := chanFromConn(r) + + defer wsConn.Close() + defer r.Close() + for { + select { + case wsData := <-wsChan: + if wsData == nil { + Logger.Infof("STDIO connection closed: D: %v, S: %v", "stdio", wsConn.RemoteAddr()) + return + } else { + w.Write(wsData) + } + case tcpData := <-stdioChan: + if tcpData == nil { + Logger.Infof("STDIO connection closed: D: %v, S: %v", "stdio", wsConn.LocalAddr()) + return + } else { + wsConn.Write(tcpData) + } + } + } +} diff --git a/pkg/remoteaccess/client.go b/pkg/remoteaccess/client.go new file mode 100644 index 00000000..25543b57 --- /dev/null +++ b/pkg/remoteaccess/client.go @@ -0,0 +1,153 @@ +package remoteaccess + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "strings" + + "github.com/gorilla/websocket" + "github.com/reubenmiller/go-c8y/pkg/c8y" + "github.com/reubenmiller/go-c8y/pkg/logger" + "github.com/reubenmiller/go-c8y/pkg/proxy" +) + +type RemoteAccessOptions struct { + ManagedObjectID string + RemoteAccessID string +} + +func parseListenerAddress(v string) (network string, addr string, err error) { + network = "tcp" + + networkTypeAddress := strings.SplitN(v, "://", 2) + switch len(networkTypeAddress) { + case 0: + err = fmt.Errorf("invalid local address") + case 1: + addr = networkTypeAddress[0] + case 2: + network = networkTypeAddress[0] + addr = networkTypeAddress[1] + } + + return network, addr, err +} + +type RemoteAccessClient struct { + client *c8y.Client + ctx RemoteAccessOptions + listener net.Listener + log logger.Logger +} + +// Create new Remote Access client to allow local clients +// to connect to a device via the Cloud Remote Access feature +func NewRemoteAccessClient(client *c8y.Client, opt RemoteAccessOptions, log logger.Logger) *RemoteAccessClient { + return &RemoteAccessClient{ + client: client, + ctx: opt, + listener: nil, + log: log, + } +} + +func (c *RemoteAccessClient) createRemoteAccessConnection() (*websocket.Conn, string, error) { + host := c.client.BaseURL.String() + wsHost := "" + if strings.HasPrefix(host, "http://") { + wsHost = "ws://" + host[7:] + } else if strings.HasPrefix(host, "https://") { + wsHost = "wss://" + host[8:] + } + remoteURL := fmt.Sprintf("%s/service/remoteaccess/client/%s/configurations/%s", strings.TrimRight(wsHost, "/"), c.ctx.ManagedObjectID, c.ctx.RemoteAccessID) + + requestHeader := http.Header{} + requestHeader.Add("Content-Type", "application/json") + + switch c.client.AuthorizationMethod { + case c8y.AuthMethodBasic: + requestHeader.Add("Authorization", c8y.NewBasicAuthString(c.client.GetTenantName(context.Background()), c.client.Username, c.client.Password)) + default: + requestHeader.Add("Authorization", "Bearer "+c.client.Token) + } + + c.log.Infof("Connection to Cumulocity IoT CRA: remote=%s, headers=%v", remoteURL, requestHeader) + + wsConn, _, err := websocket.DefaultDialer.Dial(remoteURL, requestHeader) + return wsConn, remoteURL, err +} + +// Get the listener address. Useful when using the "free port" option, and need +// to know which port the listener chose +func (c *RemoteAccessClient) GetListenerAddress() string { + if c.listener != nil { + return c.listener.Addr().String() + } + return "" +} + +// Listen and serve a single connection. It bridges between the websocket and the given reader/writer +// Typically it can be used to setup proxying to stdin/stdout +func (c *RemoteAccessClient) ListenServe(r io.ReadCloser, w io.Writer) error { + clientWsConn, remoteURL, err := c.createRemoteAccessConnection() + if err != nil { + c.log.Errorf("DIALER: %v", err.Error()) + return err + } + c.log.Infof("Proxying traffic to %v via %v for %v", remoteURL, clientWsConn.RemoteAddr(), "stdio") + + // block until finished as stdio mode can not launch multiple instances + proxy.CopyReadWriter(clientWsConn, r, w) + return nil +} + +// Start a client using which listens to either incoming requests via a TCP or Unix socket +// Set local stream address to listen to +// Example: :8080, 127.0.0.1:8080, 127.0.0.1:0 (first free port) +func (c *RemoteAccessClient) Listen(addr string) error { + network, localAddress, err := parseListenerAddress(addr) + if err != nil { + return err + } + + c.log.Infof("Creating listener. network=%s, address=%s", network, localAddress) + + l, err := net.Listen(network, localAddress) + if err != nil { + c.log.Errorf("%s LISTENER: %v", strings.ToUpper(network), err.Error()) + return err + } + + c.listener = l + return nil +} + +// Serve requests to the local TCP server or Unix socket +// The Listen must be called prior to trying to serve +func (c *RemoteAccessClient) Serve() error { + if c.listener == nil { + return fmt.Errorf("listen must be called before serve") + } + + // Close the listener when the application closes. + defer c.listener.Close() + for { + // Listen for an incoming connection. + tcpConn, err := c.listener.Accept() + if err != nil { + c.log.Errorf("ACCEPT: %v", err.Error()) + } + + clientWsConn, remoteURL, err := c.createRemoteAccessConnection() + if err != nil { + c.log.Errorf("DIALER: %v", err.Error()) + return err + } + // Handle connections in a new goroutine. + c.log.Infof("Proxying traffic to %v via %v for %v", remoteURL, clientWsConn.RemoteAddr(), tcpConn.RemoteAddr()) + go proxy.Copy(clientWsConn, tcpConn) + } +} diff --git a/pkg/wsconnadapter/wsconnadapter.go b/pkg/wsconnadapter/wsconnadapter.go new file mode 100644 index 00000000..ee3485b6 --- /dev/null +++ b/pkg/wsconnadapter/wsconnadapter.go @@ -0,0 +1,102 @@ +package wsconnadapter + +import ( + "errors" + "github.com/gorilla/websocket" + "io" + "net" + "sync" + "time" +) + +// an adapter for representing WebSocket connection as a net.Conn +// some caveats apply: https://github.com/gorilla/websocket/issues/441 + +type Adapter struct { + conn *websocket.Conn + readMutex sync.Mutex + writeMutex sync.Mutex + reader io.Reader +} + +func New(conn *websocket.Conn) *Adapter { + return &Adapter{ + conn: conn, + } +} + +func (a *Adapter) Read(b []byte) (int, error) { + // Read() can be called concurrently, and we mutate some internal state here + a.readMutex.Lock() + defer a.readMutex.Unlock() + + if a.reader == nil { + messageType, reader, err := a.conn.NextReader() + if err != nil { + return 0, err + } + + if messageType != websocket.BinaryMessage { + return 0, errors.New("unexpected websocket message type") + } + + a.reader = reader + } + + bytesRead, err := a.reader.Read(b) + if err != nil { + a.reader = nil + + // EOF for the current Websocket frame, more will probably come so.. + if err == io.EOF { + // .. we must hide this from the caller since our semantics are a + // stream of bytes across many frames + err = nil + } + } + + return bytesRead, err +} + +func (a *Adapter) Write(b []byte) (int, error) { + a.writeMutex.Lock() + defer a.writeMutex.Unlock() + + nextWriter, err := a.conn.NextWriter(websocket.BinaryMessage) + if err != nil { + return 0, err + } + + bytesWritten, err := nextWriter.Write(b) + nextWriter.Close() + + return bytesWritten, err +} + +func (a *Adapter) Close() error { + return a.conn.Close() +} + +func (a *Adapter) LocalAddr() net.Addr { + return a.conn.LocalAddr() +} + +func (a *Adapter) RemoteAddr() net.Addr { + return a.conn.RemoteAddr() +} + +func (a *Adapter) SetDeadline(t time.Time) error { + if err := a.SetReadDeadline(t); err != nil { + return err + } + + return a.SetWriteDeadline(t) +} + +func (a *Adapter) SetReadDeadline(t time.Time) error { + return a.conn.SetReadDeadline(t) +} + +func (a *Adapter) SetWriteDeadline(t time.Time) error { + return a.conn.SetWriteDeadline(t) +} From cf57cd41f217307d662a11a9d619d60e276d4d59 Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Tue, 14 May 2024 19:45:44 +0200 Subject: [PATCH 2/8] feat: support simple remote access service api --- pkg/c8y/client.go | 2 + pkg/c8y/remoteaccess.go | 108 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 pkg/c8y/remoteaccess.go diff --git a/pkg/c8y/client.go b/pkg/c8y/client.go index 12d979ba..af9b866e 100644 --- a/pkg/c8y/client.go +++ b/pkg/c8y/client.go @@ -143,6 +143,7 @@ type Client struct { Identity *IdentityService Microservice *MicroserviceService Notification2 *Notification2Service + RemoteAccess *RemoteAccessService Retention *RetentionRuleService TenantOptions *TenantOptionsService Software *InventorySoftwareService @@ -345,6 +346,7 @@ func NewClient(httpClient *http.Client, baseURL string, tenant string, username c.Microservice = (*MicroserviceService)(&c.common) c.Notification2 = (*Notification2Service)(&c.common) c.Context = (*ContextService)(&c.common) + c.RemoteAccess = (*RemoteAccessService)(&c.common) c.Retention = (*RetentionRuleService)(&c.common) c.TenantOptions = (*TenantOptionsService)(&c.common) c.Software = (*InventorySoftwareService)(&c.common) diff --git a/pkg/c8y/remoteaccess.go b/pkg/c8y/remoteaccess.go new file mode 100644 index 00000000..d6b9f7e9 --- /dev/null +++ b/pkg/c8y/remoteaccess.go @@ -0,0 +1,108 @@ +package c8y + +import ( + "context" + "fmt" + "net/http" +) + +const ( + RemoteAccessProtocolPassthrough = "PASSTHROUGH" + RemoteAccessProtocolSSH = "SSH" + RemoteAccessProtocolVNC = "VNC" + RemoteAccessProtocolTelnet = "TELNET" +) + +// RemoteAccessService +type RemoteAccessService service + +// RemoteAccessCollectionOptions remote access collection filter options +type RemoteAccessCollectionOptions struct { + // Pagination options + PaginationOptions +} + +// RemoteAccessCollection collection of remote access configurations +type RemoteAccessConfiguration struct { + ID string `json:"id"` + Name string `json:"name"` + Hostname string `json:"hostname"` + Port int `json:"port"` + Protocol string `json:"protocol"` + Credentials RemoteAccessCredentials `json:"credentials"` +} + +// RemoteAccessCredentials +type RemoteAccessCredentials struct { + Type string `json:"type"` + Username string `json:"username"` + Password string `json:"password"` + PublicKey string `json:"publicKey"` + PrivateKey string `json:"privateKey"` + HostKey string `json:"hostKey"` +} + +func (s *RemoteAccessService) path(mo_id string) string { + return fmt.Sprintf("/service/remoteaccess/devices/%s/configurations", mo_id) +} +func (s *RemoteAccessService) config_path(mo_id string, config_id string) string { + return fmt.Sprintf("/service/remoteaccess/devices/%s/configurations/%s", mo_id, config_id) +} + +// GetConfiguration return a specific remote access configuration for a given device +func (s *RemoteAccessService) GetConfiguration(ctx context.Context, mo_id, config_id string) (*RemoteAccessConfiguration, *Response, error) { + data := new(RemoteAccessConfiguration) + resp, err := s.client.SendRequest(ctx, RequestOptions{ + Method: http.MethodGet, + Path: s.config_path(mo_id, config_id), + ResponseData: data, + }) + return data, resp, err +} + +// GetConfigurations returns a collection of Cumulocity remote access configurations +// for a given managed object +func (s *RemoteAccessService) GetConfigurations(ctx context.Context, mo_id string, opt *RemoteAccessCollectionOptions) ([]RemoteAccessConfiguration, *Response, error) { + data := make([]RemoteAccessConfiguration, 0) + resp, err := s.client.SendRequest(ctx, RequestOptions{ + Method: http.MethodGet, + Path: s.path(mo_id), + Query: opt, + ResponseData: &data, + }) + return data, resp, err +} + +// DeleteConfiguration delete remote access configuration +func (s *RemoteAccessService) DeleteConfiguration(ctx context.Context, mo_id string, config_id string, opt *RemoteAccessCollectionOptions) (*Response, error) { + resp, err := s.client.SendRequest(ctx, RequestOptions{ + Method: http.MethodDelete, + Path: s.config_path(mo_id, config_id), + Query: opt, + }) + return resp, err +} + +// Create creates a new operation for a device +func (s *RemoteAccessService) Create(ctx context.Context, mo_id string, config_id string, body interface{}) (*RemoteAccessConfiguration, *Response, error) { + data := new(RemoteAccessConfiguration) + resp, err := s.client.SendRequest(ctx, RequestOptions{ + Method: http.MethodPost, + Path: s.config_path(mo_id, config_id), + Body: body, + ResponseData: data, + }) + return data, resp, err +} + +// Update updates a Cumulocity operation +func (s *RemoteAccessService) Update(ctx context.Context, mo_id string, config_id string, body *OperationUpdateOptions) (*Operation, *Response, error) { + data := new(Operation) + resp, err := s.client.SendRequest(ctx, RequestOptions{ + Method: http.MethodPut, + Path: s.config_path(mo_id, config_id), + Body: body, + ResponseData: data, + }) + return data, resp, err +} From 89e942c35918dab295a5f5d14e81ae4651cbd401 Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Thu, 16 May 2024 18:44:09 +0200 Subject: [PATCH 3/8] check if token is not empty before using it --- pkg/remoteaccess/client.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/remoteaccess/client.go b/pkg/remoteaccess/client.go index 25543b57..3e4a0e37 100644 --- a/pkg/remoteaccess/client.go +++ b/pkg/remoteaccess/client.go @@ -67,13 +67,13 @@ func (c *RemoteAccessClient) createRemoteAccessConnection() (*websocket.Conn, st requestHeader := http.Header{} requestHeader.Add("Content-Type", "application/json") - switch c.client.AuthorizationMethod { - case c8y.AuthMethodBasic: - requestHeader.Add("Authorization", c8y.NewBasicAuthString(c.client.GetTenantName(context.Background()), c.client.Username, c.client.Password)) - default: + if c.client.Token != "" { + c.log.Debug("Using bearer token") requestHeader.Add("Authorization", "Bearer "+c.client.Token) + } else { + c.log.Debug("Using basic auth") + requestHeader.Add("Authorization", c8y.NewBasicAuthString(c.client.GetTenantName(context.Background()), c.client.Username, c.client.Password)) } - c.log.Infof("Connection to Cumulocity IoT CRA: remote=%s, headers=%v", remoteURL, requestHeader) wsConn, _, err := websocket.DefaultDialer.Dial(remoteURL, requestHeader) From b3225b6e9941795cba1052b743fd958ad7702825 Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Fri, 17 May 2024 21:07:31 +0200 Subject: [PATCH 4/8] improve remote access logging --- pkg/c8y/client.go | 10 +++++----- pkg/remoteaccess/client.go | 25 +++++++++++-------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/pkg/c8y/client.go b/pkg/c8y/client.go index af9b866e..dcab48e0 100644 --- a/pkg/c8y/client.go +++ b/pkg/c8y/client.go @@ -713,7 +713,7 @@ func (c *Client) SendRequest(ctx context.Context, options RequestOptions) (*Resp return nil, dryRunErr } - localLogger.Info(c.hideSensitiveInformationIfActive(fmt.Sprintf("Headers: %v", req.Header))) + localLogger.Info(c.HideSensitiveInformationIfActive(fmt.Sprintf("Headers: %v", req.Header))) if options.PrepareRequest != nil { req, err = options.PrepareRequest(req) @@ -936,7 +936,7 @@ func (c *Client) SetBasicAuthorization(req *http.Request) { } else { headerUsername = c.Username } - Logger.Infof("Current username: %s", c.hideSensitiveInformationIfActive(headerUsername)) + Logger.Infof("Current username: %s", c.HideSensitiveInformationIfActive(headerUsername)) req.SetBasicAuth(headerUsername, c.Password) } @@ -1102,7 +1102,7 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}, middl } if req != nil { - Logger.Infof("Sending request: %s %s", req.Method, c.hideSensitiveInformationIfActive(req.URL.String())) + Logger.Infof("Sending request: %s %s", req.Method, c.HideSensitiveInformationIfActive(req.URL.String())) } // Log the body (if applicable) @@ -1289,7 +1289,7 @@ func CheckResponse(r *http.Response) error { return errorResponse } -func (c *Client) hideSensitiveInformationIfActive(message string) string { +func (c *Client) HideSensitiveInformationIfActive(message string) string { if strings.ToLower(os.Getenv(EnvVarLoggerHideSensitive)) != "true" { return message @@ -1388,7 +1388,7 @@ func (c *Client) DefaultDryRunHandler(options *RequestOptions, req *http.Request } } - Logger.Info(c.hideSensitiveInformationIfActive(message)) + Logger.Info(c.HideSensitiveInformationIfActive(message)) } type CumulocityTokenClaim struct { diff --git a/pkg/remoteaccess/client.go b/pkg/remoteaccess/client.go index 3e4a0e37..aa3a9af3 100644 --- a/pkg/remoteaccess/client.go +++ b/pkg/remoteaccess/client.go @@ -10,7 +10,6 @@ import ( "github.com/gorilla/websocket" "github.com/reubenmiller/go-c8y/pkg/c8y" - "github.com/reubenmiller/go-c8y/pkg/logger" "github.com/reubenmiller/go-c8y/pkg/proxy" ) @@ -40,17 +39,15 @@ type RemoteAccessClient struct { client *c8y.Client ctx RemoteAccessOptions listener net.Listener - log logger.Logger } // Create new Remote Access client to allow local clients // to connect to a device via the Cloud Remote Access feature -func NewRemoteAccessClient(client *c8y.Client, opt RemoteAccessOptions, log logger.Logger) *RemoteAccessClient { +func NewRemoteAccessClient(client *c8y.Client, opt RemoteAccessOptions) *RemoteAccessClient { return &RemoteAccessClient{ client: client, ctx: opt, listener: nil, - log: log, } } @@ -68,13 +65,13 @@ func (c *RemoteAccessClient) createRemoteAccessConnection() (*websocket.Conn, st requestHeader.Add("Content-Type", "application/json") if c.client.Token != "" { - c.log.Debug("Using bearer token") + c8y.Logger.Debug("Using bearer token") requestHeader.Add("Authorization", "Bearer "+c.client.Token) } else { - c.log.Debug("Using basic auth") + c8y.Logger.Debug("Using basic auth") requestHeader.Add("Authorization", c8y.NewBasicAuthString(c.client.GetTenantName(context.Background()), c.client.Username, c.client.Password)) } - c.log.Infof("Connection to Cumulocity IoT CRA: remote=%s, headers=%v", remoteURL, requestHeader) + c8y.Logger.Infof("Connecting to Cumulocity IoT: url=%s, headers=%v", remoteURL, c.client.HideSensitiveInformationIfActive(fmt.Sprintf("%v", requestHeader))) wsConn, _, err := websocket.DefaultDialer.Dial(remoteURL, requestHeader) return wsConn, remoteURL, err @@ -94,10 +91,10 @@ func (c *RemoteAccessClient) GetListenerAddress() string { func (c *RemoteAccessClient) ListenServe(r io.ReadCloser, w io.Writer) error { clientWsConn, remoteURL, err := c.createRemoteAccessConnection() if err != nil { - c.log.Errorf("DIALER: %v", err.Error()) + c8y.Logger.Errorf("DIALER: %v", err.Error()) return err } - c.log.Infof("Proxying traffic to %v via %v for %v", remoteURL, clientWsConn.RemoteAddr(), "stdio") + c8y.Logger.Infof("Proxying traffic to %v via %v for %v", remoteURL, clientWsConn.RemoteAddr(), "stdio") // block until finished as stdio mode can not launch multiple instances proxy.CopyReadWriter(clientWsConn, r, w) @@ -113,11 +110,11 @@ func (c *RemoteAccessClient) Listen(addr string) error { return err } - c.log.Infof("Creating listener. network=%s, address=%s", network, localAddress) + c8y.Logger.Infof("Creating listener. network=%s, address=%s", network, localAddress) l, err := net.Listen(network, localAddress) if err != nil { - c.log.Errorf("%s LISTENER: %v", strings.ToUpper(network), err.Error()) + c8y.Logger.Errorf("%s LISTENER: %v", strings.ToUpper(network), err.Error()) return err } @@ -138,16 +135,16 @@ func (c *RemoteAccessClient) Serve() error { // Listen for an incoming connection. tcpConn, err := c.listener.Accept() if err != nil { - c.log.Errorf("ACCEPT: %v", err.Error()) + c8y.Logger.Errorf("ACCEPT: %v", err.Error()) } clientWsConn, remoteURL, err := c.createRemoteAccessConnection() if err != nil { - c.log.Errorf("DIALER: %v", err.Error()) + c8y.Logger.Errorf("DIALER: %v", err.Error()) return err } // Handle connections in a new goroutine. - c.log.Infof("Proxying traffic to %v via %v for %v", remoteURL, clientWsConn.RemoteAddr(), tcpConn.RemoteAddr()) + c8y.Logger.Infof("Proxying traffic to %v via %v for %v", remoteURL, clientWsConn.RemoteAddr(), tcpConn.RemoteAddr()) go proxy.Copy(clientWsConn, tcpConn) } } From 70f0f33df9b7fffc17f0b12914d035d6eb75a50b Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Fri, 17 May 2024 21:11:42 +0200 Subject: [PATCH 5/8] use common package logger --- pkg/proxy/proxy.go | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 3b7eea4f..f9e88c99 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -5,17 +5,10 @@ import ( "net" "github.com/gorilla/websocket" + "github.com/reubenmiller/go-c8y/pkg/c8y" "github.com/reubenmiller/go-c8y/pkg/wsconnadapter" - - "github.com/reubenmiller/go-c8y/pkg/logger" ) -var Logger logger.Logger - -func init() { - Logger = logger.NewLogger("proxy") -} - func chanFromConn(conn io.Reader) chan []byte { c := make(chan []byte) @@ -52,14 +45,14 @@ func Copy(gwsConn *websocket.Conn, tcpConn net.Conn) { select { case wsData := <-wsChan: if wsData == nil { - Logger.Infof("Connection closed: D: %v, S: %v", tcpConn.LocalAddr(), wsConn.RemoteAddr()) + c8y.Logger.Infof("Connection closed: D: %v, S: %v", tcpConn.LocalAddr(), wsConn.RemoteAddr()) return } else { tcpConn.Write(wsData) } case tcpData := <-tcpChan: if tcpData == nil { - Logger.Infof("Connection closed: D: %v, S: %v", tcpConn.LocalAddr(), wsConn.LocalAddr()) + c8y.Logger.Infof("Connection closed: D: %v, S: %v", tcpConn.LocalAddr(), wsConn.LocalAddr()) return } else { wsConn.Write(tcpData) @@ -81,14 +74,14 @@ func CopyReadWriter(gwsConn *websocket.Conn, r io.ReadCloser, w io.Writer) { select { case wsData := <-wsChan: if wsData == nil { - Logger.Infof("STDIO connection closed: D: %v, S: %v", "stdio", wsConn.RemoteAddr()) + c8y.Logger.Infof("STDIO connection closed: D: %v, S: %v", "stdio", wsConn.RemoteAddr()) return } else { w.Write(wsData) } case tcpData := <-stdioChan: if tcpData == nil { - Logger.Infof("STDIO connection closed: D: %v, S: %v", "stdio", wsConn.LocalAddr()) + c8y.Logger.Infof("STDIO connection closed: D: %v, S: %v", "stdio", wsConn.LocalAddr()) return } else { wsConn.Write(tcpData) From e46c73085cf2dd767e47bb44e22bfceae8d012f8 Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Sat, 18 May 2024 12:01:51 +0200 Subject: [PATCH 6/8] fix import order --- pkg/wsconnadapter/wsconnadapter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/wsconnadapter/wsconnadapter.go b/pkg/wsconnadapter/wsconnadapter.go index ee3485b6..a8fd479b 100644 --- a/pkg/wsconnadapter/wsconnadapter.go +++ b/pkg/wsconnadapter/wsconnadapter.go @@ -2,11 +2,12 @@ package wsconnadapter import ( "errors" - "github.com/gorilla/websocket" "io" "net" "sync" "time" + + "github.com/gorilla/websocket" ) // an adapter for representing WebSocket connection as a net.Conn From d4b0190c1d192a6044efae30ffdcd1f4e1e0e486 Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Sat, 18 May 2024 12:02:17 +0200 Subject: [PATCH 7/8] update deprecated golangci-lint rule --- .golangci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index d106f18c..f1437cb4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,7 +4,7 @@ run: linters-settings: govet: - check-shadowing: true + shadow: true fieldalignment: true gofmt: simplify: true From 19b15ab7bc98bdfbbf7c7921a2dd4e2591fff37a Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Sat, 18 May 2024 12:18:36 +0200 Subject: [PATCH 8/8] tests: randomize failing tests --- test/c8y_test/application_test.go | 8 ++++---- test/c8y_test/deviceCredentials_test.go | 4 ++-- test/c8y_test/event_test.go | 5 +++++ test/c8y_test/user_test.go | 17 ++++++++++++++++- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/test/c8y_test/application_test.go b/test/c8y_test/application_test.go index 2b43bedf..580dd855 100644 --- a/test/c8y_test/application_test.go +++ b/test/c8y_test/application_test.go @@ -109,13 +109,13 @@ func TestApplicationService_GetApplication(t *testing.T) { func TestApplicationService_CRUD_Application(t *testing.T) { client := createTestClient() - appName := "testApplication" + appName := "testApplication" + testingutils.RandomString(7) appInfo := &c8y.Application{ - Key: "testApplicationKey", - Name: "testApplication", + Key: appName + "Key", + Name: appName, Type: "HOSTED", - ContextPath: "/testApplication", + ContextPath: "/" + appName, } // Delete application if it already exists diff --git a/test/c8y_test/deviceCredentials_test.go b/test/c8y_test/deviceCredentials_test.go index 601b304f..d1d0ab5e 100644 --- a/test/c8y_test/deviceCredentials_test.go +++ b/test/c8y_test/deviceCredentials_test.go @@ -15,7 +15,7 @@ import ( func TestDeviceCredentialsService_PollNewDeviceRequest_CreateGetDelete(t *testing.T) { client := createTestClient() - deviceID := "TEST_DEVICE1" + deviceID := "TEST_DEVICE" + testingutils.RandomString(7) // Delete the request in case it already exists client.DeviceCredentials.Delete( @@ -59,7 +59,7 @@ func TestDeviceCredentialsService_PollNewDeviceRequest_CRUD(t *testing.T) { t.Skip("The following requires device authentication") client := createTestClient() - deviceID := "TEST_DEVICE1" + deviceID := "TEST_DEVICE" + testingutils.RandomString(7) // Delete the request in case it already exists client.DeviceCredentials.Delete( diff --git a/test/c8y_test/event_test.go b/test/c8y_test/event_test.go index ba05b33a..3294e066 100644 --- a/test/c8y_test/event_test.go +++ b/test/c8y_test/event_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/reubenmiller/go-c8y/internal/pkg/testingutils" @@ -171,6 +172,10 @@ func TestEventService_DeleteEvents(t *testing.T) { testingutils.Ok(t, err) testingutils.Equals(t, http.StatusNoContent, resp.StatusCode()) + // wait for events to be deleted + // TODO: Add dynamic retry + time.Sleep(1 * time.Second) + col, resp, err = client.Event.GetEvents( context.Background(), &c8y.EventCollectionOptions{ diff --git a/test/c8y_test/user_test.go b/test/c8y_test/user_test.go index 2c7701d3..0d5dddb1 100644 --- a/test/c8y_test/user_test.go +++ b/test/c8y_test/user_test.go @@ -160,11 +160,26 @@ func TestUserService_AddUserToGroup(t *testing.T) { ) testingutils.Ok(t, err) + // Create random group + ciGroup, _, err := client.User.CreateGroup( + context.Background(), + &c8y.Group{ + Name: "cigroup" + testingutils.RandomString(7), + }, + ) + testingutils.Ok(t, err) + t.Cleanup(func() { + client.User.DeleteGroup( + context.Background(), + ciGroup.GetID(), + ) + }) + // // Get group group, resp, err := client.User.GetGroupByName( context.Background(), - "Cockpit User", + ciGroup.Name, ) testingutils.Ok(t, err) testingutils.Equals(t, http.StatusOK, resp.StatusCode())