From 04aa600222b6a5892d2c4290a2d57161135f78a1 Mon Sep 17 00:00:00 2001 From: Henry Barreto Date: Thu, 1 Aug 2024 17:30:01 -0300 Subject: [PATCH] refactor(agent,pkg): rename server package to SSH and references --- agent/main.go | 11 +- docker-compose.agent.yml | 1 + pkg/agent/agent.go | 283 +----------------- pkg/agent/modes.go | 24 +- pkg/agent/{server => ssh}/authentication.go | 2 +- pkg/agent/{ => ssh}/connector/connector.go | 0 pkg/agent/{ => ssh}/connector/docker.go | 2 +- .../modes/connector/authenticator.go | 2 +- .../modes/connector/connector.go | 0 .../modes/connector/sessioner.go | 2 +- .../modes/host/authenticator.go | 2 +- .../modes/host/authenticator_test.go | 0 .../modes/host/command/command.go | 0 .../modes/host/command/command_docker.go | 0 .../modes/host/command/command_native.go | 0 pkg/agent/{server => ssh}/modes/host/host.go | 0 .../{server => ssh}/modes/host/host_test.go | 0 pkg/agent/{server => ssh}/modes/host/pty.go | 0 .../{server => ssh}/modes/host/sessioner.go | 6 +- pkg/agent/{server => ssh}/modes/host/utils.go | 2 +- .../modes/host/utils_freebsd.go | 2 +- .../modes/mocks/authenticator.go | 0 pkg/agent/{server => ssh}/modes/modes.go | 0 pkg/agent/{server => ssh}/server.go | 6 +- pkg/agent/{server => ssh}/session.go | 2 +- pkg/agent/{ => ssh}/sftp.go | 4 +- pkg/agent/ssh/ssh.go | 188 ++++++++++++ pkg/agent/{server => ssh}/subsystem.go | 2 +- pkg/agent/{server => ssh}/utmp/utmp.go | 0 .../utmp/utmp_timeval_time32.go | 0 .../utmp/utmp_timeval_time64.go | 0 31 files changed, 239 insertions(+), 302 deletions(-) rename pkg/agent/{server => ssh}/authentication.go (95%) rename pkg/agent/{ => ssh}/connector/connector.go (100%) rename pkg/agent/{ => ssh}/connector/docker.go (99%) rename pkg/agent/{server => ssh}/modes/connector/authenticator.go (99%) rename pkg/agent/{server => ssh}/modes/connector/connector.go (100%) rename pkg/agent/{server => ssh}/modes/connector/sessioner.go (98%) rename pkg/agent/{server => ssh}/modes/host/authenticator.go (98%) rename pkg/agent/{server => ssh}/modes/host/authenticator_test.go (100%) rename pkg/agent/{server => ssh}/modes/host/command/command.go (100%) rename pkg/agent/{server => ssh}/modes/host/command/command_docker.go (100%) rename pkg/agent/{server => ssh}/modes/host/command/command_native.go (100%) rename pkg/agent/{server => ssh}/modes/host/host.go (100%) rename pkg/agent/{server => ssh}/modes/host/host_test.go (100%) rename pkg/agent/{server => ssh}/modes/host/pty.go (100%) rename pkg/agent/{server => ssh}/modes/host/sessioner.go (98%) rename pkg/agent/{server => ssh}/modes/host/utils.go (87%) rename pkg/agent/{server => ssh}/modes/host/utils_freebsd.go (87%) rename pkg/agent/{server => ssh}/modes/mocks/authenticator.go (100%) rename pkg/agent/{server => ssh}/modes/modes.go (100%) rename pkg/agent/{server => ssh}/server.go (98%) rename pkg/agent/{server => ssh}/session.go (99%) rename pkg/agent/{ => ssh}/sftp.go (95%) create mode 100644 pkg/agent/ssh/ssh.go rename pkg/agent/{server => ssh}/subsystem.go (94%) rename pkg/agent/{server => ssh}/utmp/utmp.go (100%) rename pkg/agent/{server => ssh}/utmp/utmp_timeval_time32.go (100%) rename pkg/agent/{server => ssh}/utmp/utmp_timeval_time64.go (100%) diff --git a/agent/main.go b/agent/main.go index 517c2b59a23..657aff93733 100644 --- a/agent/main.go +++ b/agent/main.go @@ -10,9 +10,10 @@ import ( "github.com/Masterminds/semver" "github.com/shellhub-io/shellhub/pkg/agent" - "github.com/shellhub-io/shellhub/pkg/agent/connector" "github.com/shellhub-io/shellhub/pkg/agent/pkg/selfupdater" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes/host/command" + "github.com/shellhub-io/shellhub/pkg/agent/ssh" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/connector" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes/host/command" "github.com/shellhub-io/shellhub/pkg/envs" "github.com/shellhub-io/shellhub/pkg/loglevel" log "github.com/sirupsen/logrus" @@ -162,14 +163,14 @@ func main() { }() } - if err := ag.Listen(ctx); err != nil { + if err := ag.ListenSSH(ctx); err != nil { log.WithError(err).WithFields(log.Fields{ "version": AgentVersion, "mode": mode, "tenant_id": cfg.TenantID, "server_address": cfg.ServerAddress, "preferred_hostname": cfg.PreferredHostname, - }).Fatal("Failed to listen for connections") + }).Fatal("Failed to listen for SSH connections") } log.WithFields(log.Fields{ @@ -266,7 +267,7 @@ func main() { Long: `Starts the SFTP server. This command is used internally by the agent and should not be used directly. It is initialized by the agent when a new SFTP session is created.`, Run: func(cmd *cobra.Command, args []string) { - agent.NewSFTPServer(command.SFTPServerMode(args[0])) + ssh.NewSFTPServer(command.SFTPServerMode(args[0])) }, }) diff --git a/docker-compose.agent.yml b/docker-compose.agent.yml index c2b5311b791..d9f0fc681ea 100644 --- a/docker-compose.agent.yml +++ b/docker-compose.agent.yml @@ -19,6 +19,7 @@ services: - SHELLHUB_PRIVATE_KEY=/go/src/github.com/shellhub-io/shellhub/agent/shellhub.key - SHELLHUB_TENANT_ID=00000000-0000-4000-0000-000000000000 - SHELLHUB_VERSION=${SHELLHUB_VERSION} + - SHELLHUB_VPN=${SHELLHUB_VPN} - SHELLHUB_LOG_LEVEL=${SHELLHUB_LOG_LEVEL} - SHELLHUB_LOG_FORMAT=${SHELLHUB_LOG_FORMAT} volumes: diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index bfee4f14f69..0e18ab18443 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -32,7 +32,7 @@ // panic(err) // } // -// ag.Listen(ctx) +// ag.ListenSSH(ctx) // } // // [ShellHub Agent]: https://github.com/shellhub-io/shellhub/tree/master/agent @@ -41,24 +41,15 @@ package agent import ( "context" "crypto/rsa" - "io" - "math/rand" - "net" - "net/http" "net/url" "os" "runtime" - "strings" - "sync/atomic" - "time" "github.com/Masterminds/semver" - "github.com/labstack/echo/v4" "github.com/pkg/errors" "github.com/shellhub-io/shellhub/pkg/agent/pkg/keygen" "github.com/shellhub-io/shellhub/pkg/agent/pkg/sysinfo" - "github.com/shellhub-io/shellhub/pkg/agent/pkg/tunnel" - "github.com/shellhub-io/shellhub/pkg/agent/server" + "github.com/shellhub-io/shellhub/pkg/agent/ssh" "github.com/shellhub-io/shellhub/pkg/api/client" "github.com/shellhub-io/shellhub/pkg/envs" "github.com/shellhub-io/shellhub/pkg/models" @@ -123,6 +114,9 @@ type Config struct { // MaxRetryConnectionTimeout specifies the maximum time, in seconds, that an agent will wait // before attempting to reconnect to the ShellHub server. Default is 60 seconds. MaxRetryConnectionTimeout int `env:"MAX_RETRY_CONNECTION_TIMEOUT,default=60" validate:"min=10,max=120"` + + // Defines if the device will try to connect to the namespace's VPN. + VPN bool `env:"VPN,default=false"` } func LoadConfigFromEnv() (*Config, map[string]interface{}, error) { @@ -169,12 +163,9 @@ type Agent struct { Identity *models.DeviceIdentity Info *models.DeviceInfo authData *models.DeviceAuthResponse - cli client.Client serverInfo *models.Info - server *server.Server - tunnel *tunnel.Tunnel - listening chan bool - closed atomic.Bool + cli client.Client + ssh *ssh.SSH mode Mode } @@ -264,8 +255,6 @@ func (a *Agent) Initialize() error { return errors.Wrap(err, "failed to authorize device") } - a.closed.Store(false) - return nil } @@ -356,263 +345,21 @@ func (a *Agent) authorize() error { return err } -func (a *Agent) isClosed() bool { - return a.closed.Load() -} - // Close closes the ShellHub Agent's listening, stoping it from receive new connection requests. func (a *Agent) Close() error { - a.closed.Store(true) - - return a.tunnel.Close() + return a.ssh.Close() } -func connHandler(serv *server.Server) func(c echo.Context) error { - return func(c echo.Context) error { - hj, ok := c.Response().Writer.(http.Hijacker) - if !ok { - return c.String(http.StatusInternalServerError, "webserver doesn't support hijacking") - } - - conn, _, err := hj.Hijack() - if err != nil { - return c.String(http.StatusInternalServerError, "failed to hijack connection") - } - - id := c.Param("id") - httpConn := c.Request().Context().Value("http-conn").(net.Conn) - serv.Sessions.Store(id, httpConn) - serv.HandleConn(httpConn) - - conn.Close() - - return nil - } -} - -func httpHandler() func(c echo.Context) error { - return func(c echo.Context) error { - replyError := func(err error, msg string, code int) error { - log.WithError(err).WithFields(log.Fields{ - "remote": c.Request().RemoteAddr, - "namespace": c.Request().Header.Get("X-Namespace"), - "path": c.Request().Header.Get("X-Path"), - "version": AgentVersion, - }).Error(msg) - - return c.String(code, msg) - } - - in, err := net.Dial("tcp", ":80") - if err != nil { - return replyError(err, "failed to connect to HTTP server on device", http.StatusInternalServerError) - } - - defer in.Close() - - url, err := url.Parse(c.Request().Header.Get("X-Path")) - if err != nil { - return replyError(err, "failed to parse URL", http.StatusInternalServerError) - } - - c.Request().URL.Scheme = "http" - c.Request().URL = url - - if err := c.Request().Write(in); err != nil { - return replyError(err, "failed to write request to the server on device", http.StatusInternalServerError) - } - - out, _, err := c.Response().Hijack() - if err != nil { - return replyError(err, "failed to hijack connection", http.StatusInternalServerError) - } - - defer out.Close() // nolint:errcheck - - if _, err := io.Copy(out, in); err != nil { - return replyError(err, "failed to copy response from device service to client", http.StatusInternalServerError) - } - - return nil - } -} - -func closeHandler(a *Agent, serv *server.Server) func(c echo.Context) error { - return func(c echo.Context) error { - id := c.Param("id") - serv.CloseSession(id) - - log.WithFields( - log.Fields{ - "id": id, - "version": AgentVersion, - "tenant_id": a.authData.Namespace, - "server_address": a.config.ServerAddress, - }, - ).Info("A tunnel connection was closed") - - return nil - } -} +// ListenSSH creates the SSH server and listening for connections. +func (a *Agent) ListenSSH(ctx context.Context) error { + a.ssh = ssh.NewSSH(a.cli, a.authData.Token, &ssh.Options{ + MaxRetryConnectionTimeout: a.config.MaxRetryConnectionTimeout, + }) -// Listen creates the SSH server and listening for connections. -func (a *Agent) Listen(ctx context.Context) error { + // TODO: Don't create the SSH server from this function, as it seems to be out its own context. a.mode.Serve(a) - a.tunnel = tunnel.NewBuilder(). - WithConnHandler(connHandler(a.server)). - WithCloseHandler(closeHandler(a, a.server)). - WithHTTPHandler(httpHandler()). - Build() - - go a.ping(ctx, AgentPingDefaultInterval) //nolint:errcheck - - ctx, cancel := context.WithCancel(ctx) - go func() { - for { - if a.isClosed() { - log.WithFields(log.Fields{ - "version": AgentVersion, - "tenant_id": a.authData.Namespace, - "server_address": a.config.ServerAddress, - }).Info("Stopped listening for connections") - - cancel() - - return - } - - namespace := a.authData.Namespace - tenantName := a.authData.Name - sshEndpoint := a.serverInfo.Endpoints.SSH - - sshid := strings.NewReplacer( - "{namespace}", namespace, - "{tenantName}", tenantName, - "{sshEndpoint}", strings.Split(sshEndpoint, ":")[0], - ).Replace("{namespace}.{tenantName}@{sshEndpoint}") - - listener, err := a.cli.NewReverseListener(ctx, a.authData.Token) - if err != nil { - log.WithError(err).WithFields(log.Fields{ - "version": AgentVersion, - "tenant_id": a.authData.Namespace, - "server_address": a.config.ServerAddress, - "ssh_server": sshEndpoint, - "sshid": sshid, - }).Error("Failed to connect to server through reverse tunnel. Retry in 10 seconds") - time.Sleep(time.Second * 10) - - continue - } - - log.WithFields(log.Fields{ - "namespace": namespace, - "hostname": tenantName, - "server_address": a.config.ServerAddress, - "ssh_server": sshEndpoint, - "sshid": sshid, - }).Info("Server connection established") - - a.listening <- true - - { - // NOTE: Tunnel'll only realize that it lost its connection to the ShellHub SSH when the next - // "keep-alive" connection fails. As a result, it will take this interval to reconnect to its server. - err := a.tunnel.Listen(listener) - - log.WithError(err).WithFields(log.Fields{ - "namespace": namespace, - "hostname": tenantName, - "server_address": a.config.ServerAddress, - "ssh_server": sshEndpoint, - "sshid": sshid, - }).Info("Tunnel listener closed") - - listener.Close() // nolint:errcheck - } - - a.listening <- false - } - }() - - <-ctx.Done() - - return a.Close() -} - -// AgentPingDefaultInterval is the default time interval between ping on agent. -const AgentPingDefaultInterval = 10 * time.Minute - -// ping sends an authorization request to the ShellHub server at each interval. -// A random value between 10 and [config.MaxRetryConnectionTimeout] seconds is added to the interval -// each time the ticker is executed. -// -// Ping only sends requests to the server if the agent is listening for connections. If the agent is not -// listening, the ping process will be stopped. When the interval is 0, the default value is 10 minutes. -func (a *Agent) ping(ctx context.Context, interval time.Duration) error { - a.listening = make(chan bool) - - if interval == 0 { - interval = AgentPingDefaultInterval - } - - <-a.listening // NOTE: wait for the first connection to start to ping the server. - ticker := time.NewTicker(interval) - - for { - if a.isClosed() { - return nil - } - - select { - case <-ctx.Done(): - log.WithFields(log.Fields{ - "version": AgentVersion, - "tenant_id": a.authData.Namespace, - "server_address": a.config.ServerAddress, - }).Debug("stopped pinging server due to context cancellation") - - return nil - case ok := <-a.listening: - if ok { - log.WithFields(log.Fields{ - "version": AgentVersion, - "tenant_id": a.authData.Namespace, - "server_address": a.config.ServerAddress, - "timestamp": time.Now(), - }).Debug("Starting the ping interval to server") - - ticker.Reset(interval) - } else { - log.WithFields(log.Fields{ - "version": AgentVersion, - "tenant_id": a.authData.Namespace, - "server_address": a.config.ServerAddress, - "timestamp": time.Now(), - }).Debug("Stopped pinging server due listener status") - - ticker.Stop() - } - case <-ticker.C: - if err := a.authorize(); err != nil { - a.server.SetDeviceName(a.authData.Name) - } - - log.WithFields(log.Fields{ - "version": AgentVersion, - "tenant_id": a.authData.Namespace, - "server_address": a.config.ServerAddress, - "name": a.authData.Name, - "hostname": a.config.PreferredHostname, - "identity": a.config.PreferredIdentity, - "timestamp": time.Now(), - }).Info("Ping") - - randTimeout := time.Duration(rand.Intn(a.config.MaxRetryConnectionTimeout-10)+10) * time.Second - ticker.Reset(interval + randTimeout) - } - } + return a.ssh.Listen(ctx) } // CheckUpdate gets the ShellHub's server version. diff --git a/pkg/agent/modes.go b/pkg/agent/modes.go index aff45bf9112..299825670db 100644 --- a/pkg/agent/modes.go +++ b/pkg/agent/modes.go @@ -6,9 +6,9 @@ import ( dockerclient "github.com/docker/docker/client" "github.com/shellhub-io/shellhub/pkg/agent/pkg/sysinfo" - "github.com/shellhub-io/shellhub/pkg/agent/server" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes/connector" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes/host" + ssh "github.com/shellhub-io/shellhub/pkg/agent/ssh" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes/connector" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes/host" ) type Info struct { @@ -38,20 +38,20 @@ type HostMode struct{} var _ Mode = new(HostMode) func (m *HostMode) Serve(agent *Agent) { - agent.server = server.NewServer( + agent.ssh.Server = ssh.NewServer( agent.cli, &host.Mode{ Authenticator: *host.NewAuthenticator(agent.cli, agent.authData, agent.config.SingleUserPassword, &agent.authData.Name), Sessioner: *host.NewSessioner(&agent.authData.Name, make(map[string]*exec.Cmd)), }, - &server.Config{ + &ssh.Config{ PrivateKey: agent.config.PrivateKey, KeepAliveInterval: agent.config.KeepAliveInterval, - Features: server.LocalPortForwardFeature, + Features: ssh.LocalPortForwardFeature, }, ) - agent.server.SetDeviceName(agent.authData.Name) + agent.ssh.Server.SetDeviceName(agent.authData.Name) } func (m *HostMode) GetInfo() (*Info, error) { @@ -90,21 +90,21 @@ func (m *ConnectorMode) Serve(agent *Agent) { // communication between the server and the agent when the container name on the host changes. This information is // saved inside the device's identity, avoiding significant changes in the current state of the agent. // TODO: Evaluate if we can use another field than "MAC" to store the container ID. - agent.server = server.NewServer( + agent.ssh.Server = ssh.NewServer( agent.cli, &connector.Mode{ Authenticator: *connector.NewAuthenticator(agent.cli, m.cli, agent.authData, &agent.Identity.MAC), Sessioner: *connector.NewSessioner(&agent.Identity.MAC, m.cli), }, - &server.Config{ + &ssh.Config{ PrivateKey: agent.config.PrivateKey, KeepAliveInterval: agent.config.KeepAliveInterval, - Features: server.NoFeature, + Features: ssh.NoFeature, }, ) - agent.server.SetContainerID(agent.Identity.MAC) - agent.server.SetDeviceName(agent.authData.Name) + agent.ssh.Server.SetContainerID(agent.Identity.MAC) + agent.ssh.Server.SetDeviceName(agent.authData.Name) } func (m *ConnectorMode) GetInfo() (*Info, error) { diff --git a/pkg/agent/server/authentication.go b/pkg/agent/ssh/authentication.go similarity index 95% rename from pkg/agent/server/authentication.go rename to pkg/agent/ssh/authentication.go index b581ffdad68..7136b0cda28 100644 --- a/pkg/agent/server/authentication.go +++ b/pkg/agent/ssh/authentication.go @@ -1,4 +1,4 @@ -package server +package ssh import ( gliderssh "github.com/gliderlabs/ssh" diff --git a/pkg/agent/connector/connector.go b/pkg/agent/ssh/connector/connector.go similarity index 100% rename from pkg/agent/connector/connector.go rename to pkg/agent/ssh/connector/connector.go diff --git a/pkg/agent/connector/docker.go b/pkg/agent/ssh/connector/docker.go similarity index 99% rename from pkg/agent/connector/docker.go rename to pkg/agent/ssh/connector/docker.go index 7f3f03225c7..bbec5e36762 100644 --- a/pkg/agent/connector/docker.go +++ b/pkg/agent/ssh/connector/docker.go @@ -273,7 +273,7 @@ func initContainerAgent(ctx context.Context, cli *dockerclient.Client, container // NOTICE(r): listing for connection and wait for a channel message to close the agent. It will receives // this mensagem when something out of this goroutine send a `done`, what will cause the agent closes // and no more connection to be allowed until it be started again. - if err := ag.Listen(ctx); err != nil { + if err := ag.ListenSSH(ctx); err != nil { log.WithError(err).WithFields(log.Fields{ "id": container.ID, "identity": cfg.PreferredIdentity, diff --git a/pkg/agent/server/modes/connector/authenticator.go b/pkg/agent/ssh/modes/connector/authenticator.go similarity index 99% rename from pkg/agent/server/modes/connector/authenticator.go rename to pkg/agent/ssh/modes/connector/authenticator.go index 8d8391b260a..b9d09e7a784 100644 --- a/pkg/agent/server/modes/connector/authenticator.go +++ b/pkg/agent/ssh/modes/connector/authenticator.go @@ -13,7 +13,7 @@ import ( dockerclient "github.com/docker/docker/client" gliderssh "github.com/gliderlabs/ssh" "github.com/shellhub-io/shellhub/pkg/agent/pkg/osauth" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes" "github.com/shellhub-io/shellhub/pkg/api/client" "github.com/shellhub-io/shellhub/pkg/models" log "github.com/sirupsen/logrus" diff --git a/pkg/agent/server/modes/connector/connector.go b/pkg/agent/ssh/modes/connector/connector.go similarity index 100% rename from pkg/agent/server/modes/connector/connector.go rename to pkg/agent/ssh/modes/connector/connector.go diff --git a/pkg/agent/server/modes/connector/sessioner.go b/pkg/agent/ssh/modes/connector/sessioner.go similarity index 98% rename from pkg/agent/server/modes/connector/sessioner.go rename to pkg/agent/ssh/modes/connector/sessioner.go index 20e7f3d7de7..dce9572f306 100644 --- a/pkg/agent/server/modes/connector/sessioner.go +++ b/pkg/agent/ssh/modes/connector/sessioner.go @@ -10,7 +10,7 @@ import ( "github.com/docker/docker/pkg/stdcopy" gliderssh "github.com/gliderlabs/ssh" "github.com/shellhub-io/shellhub/pkg/agent/pkg/osauth" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes" ) var ErrUserNotFound = errors.New("user not found on context") diff --git a/pkg/agent/server/modes/host/authenticator.go b/pkg/agent/ssh/modes/host/authenticator.go similarity index 98% rename from pkg/agent/server/modes/host/authenticator.go rename to pkg/agent/ssh/modes/host/authenticator.go index c4d10e7e2a6..4c11f89cfac 100644 --- a/pkg/agent/server/modes/host/authenticator.go +++ b/pkg/agent/ssh/modes/host/authenticator.go @@ -9,7 +9,7 @@ import ( gliderssh "github.com/gliderlabs/ssh" "github.com/shellhub-io/shellhub/pkg/agent/pkg/osauth" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes" "github.com/shellhub-io/shellhub/pkg/api/client" "github.com/shellhub-io/shellhub/pkg/models" log "github.com/sirupsen/logrus" diff --git a/pkg/agent/server/modes/host/authenticator_test.go b/pkg/agent/ssh/modes/host/authenticator_test.go similarity index 100% rename from pkg/agent/server/modes/host/authenticator_test.go rename to pkg/agent/ssh/modes/host/authenticator_test.go diff --git a/pkg/agent/server/modes/host/command/command.go b/pkg/agent/ssh/modes/host/command/command.go similarity index 100% rename from pkg/agent/server/modes/host/command/command.go rename to pkg/agent/ssh/modes/host/command/command.go diff --git a/pkg/agent/server/modes/host/command/command_docker.go b/pkg/agent/ssh/modes/host/command/command_docker.go similarity index 100% rename from pkg/agent/server/modes/host/command/command_docker.go rename to pkg/agent/ssh/modes/host/command/command_docker.go diff --git a/pkg/agent/server/modes/host/command/command_native.go b/pkg/agent/ssh/modes/host/command/command_native.go similarity index 100% rename from pkg/agent/server/modes/host/command/command_native.go rename to pkg/agent/ssh/modes/host/command/command_native.go diff --git a/pkg/agent/server/modes/host/host.go b/pkg/agent/ssh/modes/host/host.go similarity index 100% rename from pkg/agent/server/modes/host/host.go rename to pkg/agent/ssh/modes/host/host.go diff --git a/pkg/agent/server/modes/host/host_test.go b/pkg/agent/ssh/modes/host/host_test.go similarity index 100% rename from pkg/agent/server/modes/host/host_test.go rename to pkg/agent/ssh/modes/host/host_test.go diff --git a/pkg/agent/server/modes/host/pty.go b/pkg/agent/ssh/modes/host/pty.go similarity index 100% rename from pkg/agent/server/modes/host/pty.go rename to pkg/agent/ssh/modes/host/pty.go diff --git a/pkg/agent/server/modes/host/sessioner.go b/pkg/agent/ssh/modes/host/sessioner.go similarity index 98% rename from pkg/agent/server/modes/host/sessioner.go rename to pkg/agent/ssh/modes/host/sessioner.go index 04bb22811e4..7d7807dfbe9 100644 --- a/pkg/agent/server/modes/host/sessioner.go +++ b/pkg/agent/ssh/modes/host/sessioner.go @@ -12,9 +12,9 @@ import ( gliderssh "github.com/gliderlabs/ssh" "github.com/shellhub-io/shellhub/pkg/agent/pkg/osauth" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes/host/command" - "github.com/shellhub-io/shellhub/pkg/agent/server/utmp" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes/host/command" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/utmp" log "github.com/sirupsen/logrus" gossh "golang.org/x/crypto/ssh" ) diff --git a/pkg/agent/server/modes/host/utils.go b/pkg/agent/ssh/modes/host/utils.go similarity index 87% rename from pkg/agent/server/modes/host/utils.go rename to pkg/agent/ssh/modes/host/utils.go index 10c4215daa0..f77df9aa712 100644 --- a/pkg/agent/server/modes/host/utils.go +++ b/pkg/agent/ssh/modes/host/utils.go @@ -7,7 +7,7 @@ import ( "os/exec" "github.com/shellhub-io/shellhub/pkg/agent/pkg/osauth" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes/host/command" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes/host/command" ) func newShellCmd(deviceName string, username string, term string, envs []string) *exec.Cmd { diff --git a/pkg/agent/server/modes/host/utils_freebsd.go b/pkg/agent/ssh/modes/host/utils_freebsd.go similarity index 87% rename from pkg/agent/server/modes/host/utils_freebsd.go rename to pkg/agent/ssh/modes/host/utils_freebsd.go index f99624844aa..27249ee0e23 100644 --- a/pkg/agent/server/modes/host/utils_freebsd.go +++ b/pkg/agent/ssh/modes/host/utils_freebsd.go @@ -7,7 +7,7 @@ import ( "os/exec" "github.com/shellhub-io/shellhub/pkg/agent/pkg/osauth" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes/host/command" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes/host/command" ) func newShellCmd(deviceName string, username string, term string, envs []string) *exec.Cmd { diff --git a/pkg/agent/server/modes/mocks/authenticator.go b/pkg/agent/ssh/modes/mocks/authenticator.go similarity index 100% rename from pkg/agent/server/modes/mocks/authenticator.go rename to pkg/agent/ssh/modes/mocks/authenticator.go diff --git a/pkg/agent/server/modes/modes.go b/pkg/agent/ssh/modes/modes.go similarity index 100% rename from pkg/agent/server/modes/modes.go rename to pkg/agent/ssh/modes/modes.go diff --git a/pkg/agent/server/server.go b/pkg/agent/ssh/server.go similarity index 98% rename from pkg/agent/server/server.go rename to pkg/agent/ssh/server.go index 751312cd88b..910c5a64f71 100644 --- a/pkg/agent/server/server.go +++ b/pkg/agent/ssh/server.go @@ -1,4 +1,4 @@ -package server +package ssh import ( "net" @@ -7,8 +7,8 @@ import ( "time" gliderssh "github.com/gliderlabs/ssh" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes/host" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes/host" "github.com/shellhub-io/shellhub/pkg/api/client" log "github.com/sirupsen/logrus" gossh "golang.org/x/crypto/ssh" diff --git a/pkg/agent/server/session.go b/pkg/agent/ssh/session.go similarity index 99% rename from pkg/agent/server/session.go rename to pkg/agent/ssh/session.go index fd6a2c9cf37..2f4451ef8b1 100644 --- a/pkg/agent/server/session.go +++ b/pkg/agent/ssh/session.go @@ -1,4 +1,4 @@ -package server +package ssh import ( "fmt" diff --git a/pkg/agent/sftp.go b/pkg/agent/ssh/sftp.go similarity index 95% rename from pkg/agent/sftp.go rename to pkg/agent/ssh/sftp.go index 80c36cd8f0c..e8dee071e70 100644 --- a/pkg/agent/sftp.go +++ b/pkg/agent/ssh/sftp.go @@ -1,4 +1,4 @@ -package agent +package ssh import ( "errors" @@ -9,7 +9,7 @@ import ( "syscall" "github.com/pkg/sftp" - "github.com/shellhub-io/shellhub/pkg/agent/server/modes/host/command" + "github.com/shellhub-io/shellhub/pkg/agent/ssh/modes/host/command" ) type pipe struct { diff --git a/pkg/agent/ssh/ssh.go b/pkg/agent/ssh/ssh.go new file mode 100644 index 00000000000..08512c30f87 --- /dev/null +++ b/pkg/agent/ssh/ssh.go @@ -0,0 +1,188 @@ +package ssh + +import ( + "context" + "io" + "net" + "net/http" + "net/url" + "time" + + "github.com/labstack/echo/v4" + "github.com/shellhub-io/shellhub/pkg/agent/pkg/tunnel" + "github.com/shellhub-io/shellhub/pkg/api/client" + log "github.com/sirupsen/logrus" +) + +// SSHPingDefaultInterval is the default time interval between ping on agent. +const SSHPingDefaultInterval = 10 * time.Minute + +type Options struct { + // MaxRetryConnectionTimeout specifies the maximum time, in seconds, that an agent will wait + // before attempting to reconnect to the ShellHub server. Default is 60 seconds. + MaxRetryConnectionTimeout int +} + +type SSH struct { + Options *Options + Server *Server + tunnel *tunnel.Tunnel + cli client.Client + token string + listening chan bool +} + +// NewSSH creates a new instance of SSH server. +func NewSSH(cli client.Client, token string, options *Options) *SSH { + return &SSH{ + Options: options, + cli: cli, + token: token, + listening: make(chan bool), + } +} + +func connSSHHandler(serv *Server) func(c echo.Context) error { + return func(c echo.Context) error { + hj, ok := c.Response().Writer.(http.Hijacker) + if !ok { + return c.String(http.StatusInternalServerError, "webserver doesn't support hijacking") + } + + conn, _, err := hj.Hijack() + if err != nil { + return c.String(http.StatusInternalServerError, "failed to hijack connection") + } + + id := c.Param("id") + httpConn := c.Request().Context().Value("http-conn").(net.Conn) + serv.Sessions.Store(id, httpConn) + serv.HandleConn(httpConn) + + conn.Close() + + return nil + } +} + +func httpHandler() func(c echo.Context) error { + return func(c echo.Context) error { + replyError := func(err error, msg string, code int) error { + /*log.WithError(err).WithFields(log.Fields{ + "remote": c.Request().RemoteAddr, + "namespace": c.Request().Header.Get("X-Namespace"), + "path": c.Request().Header.Get("X-Path"), + "version": AgentVersion, + }).Error(msg)*/ + + return c.String(code, msg) + } + + in, err := net.Dial("tcp", ":80") + if err != nil { + return replyError(err, "failed to connect to HTTP server on device", http.StatusInternalServerError) + } + + defer in.Close() + + url, err := url.Parse(c.Request().Header.Get("X-Path")) + if err != nil { + return replyError(err, "failed to parse URL", http.StatusInternalServerError) + } + + c.Request().URL.Scheme = "http" + c.Request().URL = url + + if err := c.Request().Write(in); err != nil { + return replyError(err, "failed to write request to the server on device", http.StatusInternalServerError) + } + + out, _, err := c.Response().Hijack() + if err != nil { + return replyError(err, "failed to hijack connection", http.StatusInternalServerError) + } + + defer out.Close() // nolint:errcheck + + if _, err := io.Copy(out, in); err != nil { + return replyError(err, "failed to copy response from device service to client", http.StatusInternalServerError) + } + + return nil + } +} + +func closeSSHHandler(serv *Server) func(c echo.Context) error { + return func(c echo.Context) error { + id := c.Param("id") + serv.CloseSession(id) + + /*log.WithFields( + log.Fields{ + "id": id, + "version": AgentVersion, + "tenant_id": a.authData.Namespace, + "server_address": a.config.ServerAddress, + }, + ).Info("A tunnel connection was closed")*/ + + return nil + } +} + +// Close closes the ShellHub Agent's listening, stoping it from receive new connection requests. +func (s *SSH) Close() error { + return s.tunnel.Close() +} + +func (s *SSH) Listen(ctx context.Context) error { + s.tunnel = tunnel.NewBuilder(). + WithConnHandler(connSSHHandler(s.Server)). + WithCloseHandler(closeSSHHandler(s.Server)). + WithHTTPHandler(httpHandler()). + Build() + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + go func() { + for { + /*if a.isClosed() { + log.WithFields(log.Fields{ + "version": AgentVersion, + "tenant_id": a.authData.Namespace, + "server_address": a.config.ServerAddress, + }).Info("Stopped listening for connections") + + cancel() + + return + }*/ + + listener, err := s.cli.NewReverseListener(ctx, s.token) + if err != nil { + log.Error("Failed to connect to server through reverse tunnel. Retry in 10 seconds") + + time.Sleep(time.Second * 10) + + continue + } + + log.Info("SSH server connection established") + + { + // NOTE: Tunnel'll only realize that it lost its connection to the ShellHub SSH when the next + // "keep-alive" connection fails. As a result, it will take this interval to reconnect to its server. + err := s.tunnel.Listen(listener) + + log.WithError(err).Warn("Tunnel listener closed") + + listener.Close() // nolint:errcheck + } + } + }() + + <-ctx.Done() + + return s.Close() +} diff --git a/pkg/agent/server/subsystem.go b/pkg/agent/ssh/subsystem.go similarity index 94% rename from pkg/agent/server/subsystem.go rename to pkg/agent/ssh/subsystem.go index 63dc97db0fe..d09c67d28b3 100644 --- a/pkg/agent/server/subsystem.go +++ b/pkg/agent/ssh/subsystem.go @@ -1,4 +1,4 @@ -package server +package ssh import ( gliderssh "github.com/gliderlabs/ssh" diff --git a/pkg/agent/server/utmp/utmp.go b/pkg/agent/ssh/utmp/utmp.go similarity index 100% rename from pkg/agent/server/utmp/utmp.go rename to pkg/agent/ssh/utmp/utmp.go diff --git a/pkg/agent/server/utmp/utmp_timeval_time32.go b/pkg/agent/ssh/utmp/utmp_timeval_time32.go similarity index 100% rename from pkg/agent/server/utmp/utmp_timeval_time32.go rename to pkg/agent/ssh/utmp/utmp_timeval_time32.go diff --git a/pkg/agent/server/utmp/utmp_timeval_time64.go b/pkg/agent/ssh/utmp/utmp_timeval_time64.go similarity index 100% rename from pkg/agent/server/utmp/utmp_timeval_time64.go rename to pkg/agent/ssh/utmp/utmp_timeval_time64.go