diff --git a/.github/workflows/coder.yaml b/.github/workflows/coder.yaml index dbaea72aa1..1ec2e82430 100644 --- a/.github/workflows/coder.yaml +++ b/.github/workflows/coder.yaml @@ -158,6 +158,10 @@ jobs: terraform_version: 1.1.2 terraform_wrapper: false + - name: Install socat + if: runner.os == 'Linux' + run: sudo apt-get install -y socat + - name: Test with Mock Database shell: bash env: diff --git a/agent/agent.go b/agent/agent.go index 141075c8ce..81eeb1e850 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -21,6 +21,8 @@ import ( "github.com/coder/coder/pty" "github.com/coder/retry" + "github.com/pkg/sftp" + "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" "golang.org/x/xerrors" @@ -121,7 +123,7 @@ func (a *agent) handlePeerConn(ctx context.Context, conn *peer.Conn) { switch channel.Protocol() { case "ssh": - a.sshServer.HandleConn(channel.NetConn()) + go a.sshServer.HandleConn(channel.NetConn()) default: a.options.Logger.Warn(ctx, "unhandled protocol from channel", slog.F("protocol", channel.Protocol()), @@ -146,7 +148,10 @@ func (a *agent) init(ctx context.Context) { sshLogger := a.options.Logger.Named("ssh-server") forwardHandler := &ssh.ForwardedTCPHandler{} a.sshServer = &ssh.Server{ - ChannelHandlers: ssh.DefaultChannelHandlers, + ChannelHandlers: map[string]ssh.ChannelHandler{ + "direct-tcpip": ssh.DirectTCPIPHandler, + "session": ssh.DefaultSessionHandler, + }, ConnectionFailedCallback: func(conn net.Conn, err error) { sshLogger.Info(ctx, "ssh connection ended", slog.Error(err)) }, @@ -185,61 +190,54 @@ func (a *agent) init(ctx context.Context) { NoClientAuth: true, } }, + SubsystemHandlers: map[string]ssh.SubsystemHandler{ + "sftp": func(session ssh.Session) { + server, err := sftp.NewServer(session) + if err != nil { + a.options.Logger.Debug(session.Context(), "initialize sftp server", slog.Error(err)) + return + } + defer server.Close() + err = server.Serve() + if errors.Is(err, io.EOF) { + return + } + a.options.Logger.Debug(session.Context(), "sftp server exited with error", slog.Error(err)) + }, + }, } go a.run(ctx) } func (a *agent) handleSSHSession(session ssh.Session) error { - var ( - command string - args = []string{} - err error - ) - currentUser, err := user.Current() if err != nil { return xerrors.Errorf("get current user: %w", err) } username := currentUser.Username - // gliderlabs/ssh returns a command slice of zero - // when a shell is requested. - if len(session.Command()) == 0 { - command, err = usershell.Get(username) - if err != nil { - return xerrors.Errorf("get user shell: %w", err) - } - } else { - command = session.Command()[0] - if len(session.Command()) > 1 { - args = session.Command()[1:] - } + shell, err := usershell.Get(username) + if err != nil { + return xerrors.Errorf("get user shell: %w", err) } - signals := make(chan ssh.Signal) - breaks := make(chan bool) - defer close(signals) - defer close(breaks) - go func() { - for { - select { - case <-session.Context().Done(): - return - // Ignore signals and breaks for now! - case <-signals: - case <-breaks: - } - } - }() + // gliderlabs/ssh returns a command slice of zero + // when a shell is requested. + command := session.RawCommand() + if len(session.Command()) == 0 { + command = shell + } - cmd := exec.CommandContext(session.Context(), command, args...) + // OpenSSH executes all commands with the users current shell. + // We replicate that behavior for IDE support. + cmd := exec.CommandContext(session.Context(), shell, "-c", command) cmd.Env = append(os.Environ(), session.Environ()...) executablePath, err := os.Executable() if err != nil { return xerrors.Errorf("getting os executable: %w", err) } - cmd.Env = append(session.Environ(), fmt.Sprintf(`GIT_SSH_COMMAND="%s gitssh --"`, executablePath)) + cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_SSH_COMMAND="%s gitssh --"`, executablePath)) sshPty, windowSize, isPty := session.Pty() if isPty { @@ -268,7 +266,7 @@ func (a *agent) handleSSHSession(session ssh.Session) error { } cmd.Stdout = session - cmd.Stderr = session + cmd.Stderr = session.Stderr() // This blocks forever until stdin is received if we don't // use StdinPipe. It's unknown what causes this. stdinPipe, err := cmd.StdinPipe() @@ -282,8 +280,7 @@ func (a *agent) handleSSHSession(session ssh.Session) error { if err != nil { return xerrors.Errorf("start: %w", err) } - _ = cmd.Wait() - return nil + return cmd.Wait() } // isClosed returns whether the API is closed or not. diff --git a/agent/agent_test.go b/agent/agent_test.go index 0ceea3830c..f4c7d266f2 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -5,13 +5,16 @@ import ( "fmt" "io" "net" + "os" "os/exec" + "path/filepath" "runtime" "strconv" "strings" "testing" "github.com/pion/webrtc/v3" + "github.com/pkg/sftp" "github.com/stretchr/testify/require" "go.uber.org/goleak" "golang.org/x/crypto/ssh" @@ -94,6 +97,7 @@ func TestAgent(t *testing.T) { local, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) + defer local.Close() tcpAddr, valid = local.Addr().(*net.TCPAddr) require.True(t, valid) localPort := tcpAddr.Port @@ -113,6 +117,21 @@ func TestAgent(t *testing.T) { conn.Close() <-done }) + + t.Run("SFTP", func(t *testing.T) { + t.Parallel() + sshClient, err := setupAgent(t).SSHClient() + require.NoError(t, err) + client, err := sftp.NewClient(sshClient) + require.NoError(t, err) + tempFile := filepath.Join(t.TempDir(), "sftp") + file, err := client.Create(tempFile) + require.NoError(t, err) + err = file.Close() + require.NoError(t, err) + _, err = os.Stat(tempFile) + require.NoError(t, err) + }) } func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd { diff --git a/cli/cliui/resources_test.go b/cli/cliui/resources_test.go index 2711a695c8..95a7d7cb01 100644 --- a/cli/cliui/resources_test.go +++ b/cli/cliui/resources_test.go @@ -4,11 +4,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/coder/coder/cli/cliui" "github.com/coder/coder/coderd/database" "github.com/coder/coder/codersdk" "github.com/coder/coder/pty/ptytest" - "github.com/stretchr/testify/require" ) func TestWorkspaceResources(t *testing.T) { diff --git a/go.mod b/go.mod index bf86f67a78..833debd678 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/fatih/color v1.13.0 github.com/gliderlabs/ssh v0.3.3 github.com/go-chi/chi/v5 v5.0.7 + github.com/go-chi/httprate v0.5.3 github.com/go-chi/render v1.0.1 github.com/go-playground/validator/v10 v10.10.1 github.com/gohugoio/hugo v0.96.0 @@ -60,6 +61,7 @@ require ( github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f github.com/hashicorp/terraform-exec v0.15.0 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 + github.com/jedib0t/go-pretty/v6 v6.3.0 github.com/justinas/nosurf v1.1.1 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/lib/pq v1.10.5 @@ -72,7 +74,9 @@ require ( github.com/pion/transport v0.13.0 github.com/pion/webrtc/v3 v3.1.27 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 + github.com/pkg/sftp v1.13.4 github.com/quasilyte/go-ruleguard/dsl v0.3.19 + github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.26.1 github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 @@ -83,20 +87,19 @@ require ( go.uber.org/atomic v1.9.0 go.uber.org/goleak v1.1.12 golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd + golang.org/x/mod v0.5.1 golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 google.golang.org/api v0.74.0 google.golang.org/protobuf v1.28.0 + gopkg.in/DataDog/dd-trace-go.v1 v1.37.1 nhooyr.io/websocket v1.8.7 storj.io/drpc v0.0.30 ) -require github.com/go-chi/httprate v0.5.3 - -require github.com/jedib0t/go-pretty/v6 v6.3.0 - require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/BurntSushi/toml v1.0.0 // indirect @@ -171,6 +174,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.15.0 // indirect + github.com/kr/fs v0.1.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/lucas-clemente/quic-go v0.25.1-0.20220307142123-ad1cb27c1b64 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -222,7 +226,6 @@ require ( github.com/prometheus/procfs v0.7.3 // indirect github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/robfig/cron/v3 v3.0.1 github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/afero v1.8.1 // indirect @@ -235,16 +238,13 @@ require ( github.com/zclconf/go-cty v1.10.0 // indirect github.com/zeebo/errs v1.2.2 // indirect go.opencensus.io v0.23.0 // indirect - golang.org/x/mod v0.5.1 golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect golang.org/x/tools v0.1.9 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb // indirect google.golang.org/grpc v1.45.0 // indirect - gopkg.in/DataDog/dd-trace-go.v1 v1.37.1 gopkg.in/coreos/go-oidc.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect diff --git a/go.sum b/go.sum index 861b43638d..888473c4ac 100644 --- a/go.sum +++ b/go.sum @@ -1103,6 +1103,7 @@ github.com/klauspost/crc32 v1.2.0/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3H github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -1438,6 +1439,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg= +github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/peer/channel.go b/peer/channel.go index 732a6a1c1d..ca82887f2f 100644 --- a/peer/channel.go +++ b/peer/channel.go @@ -21,7 +21,7 @@ const ( // For some reason messages larger just don't work... // This shouldn't be a huge deal for real-world usage. // See: https://github.com/pion/datachannel/issues/59 - maxMessageLength = 32 * 1024 // 32 KB + maxMessageLength = 64 * 1024 // 64 KB ) // newChannel creates a new channel and initializes it. @@ -145,7 +145,9 @@ func (c *Channel) init() { if c.opts.Unordered { c.reader = c.rwc } else { - c.reader = bufio.NewReader(c.rwc) + // This must be the max message length otherwise a short + // buffer error can occur. + c.reader = bufio.NewReaderSize(c.rwc, maxMessageLength) } close(c.opened) })