2022-02-19 05:13:32 +00:00
|
|
|
package agent
|
|
|
|
|
|
|
|
import (
|
2022-11-24 12:22:20 +00:00
|
|
|
"bufio"
|
2022-02-19 05:13:32 +00:00
|
|
|
"context"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
2022-09-01 01:09:44 +00:00
|
|
|
"encoding/binary"
|
2022-04-29 22:30:10 +00:00
|
|
|
"encoding/json"
|
2022-02-19 05:13:32 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net"
|
2022-10-06 12:38:22 +00:00
|
|
|
"net/http"
|
2022-09-01 01:09:44 +00:00
|
|
|
"net/netip"
|
2022-03-22 19:17:50 +00:00
|
|
|
"os"
|
2022-02-19 05:13:32 +00:00
|
|
|
"os/exec"
|
|
|
|
"os/user"
|
2022-05-02 16:36:51 +00:00
|
|
|
"path/filepath"
|
2022-04-25 18:30:39 +00:00
|
|
|
"runtime"
|
2022-04-29 22:30:10 +00:00
|
|
|
"strconv"
|
2022-04-25 19:41:52 +00:00
|
|
|
"strings"
|
2022-02-19 05:13:32 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2022-04-29 22:30:10 +00:00
|
|
|
"github.com/armon/circbuf"
|
2022-05-24 21:03:42 +00:00
|
|
|
"github.com/gliderlabs/ssh"
|
2022-04-29 22:30:10 +00:00
|
|
|
"github.com/google/uuid"
|
2022-05-24 21:03:42 +00:00
|
|
|
"github.com/pkg/sftp"
|
2022-10-25 00:46:24 +00:00
|
|
|
"github.com/spf13/afero"
|
2022-04-25 18:30:39 +00:00
|
|
|
"go.uber.org/atomic"
|
2022-05-24 21:03:42 +00:00
|
|
|
gossh "golang.org/x/crypto/ssh"
|
2022-12-13 19:28:07 +00:00
|
|
|
"golang.org/x/exp/slices"
|
2022-05-24 21:03:42 +00:00
|
|
|
"golang.org/x/xerrors"
|
2022-09-05 22:15:49 +00:00
|
|
|
"tailscale.com/net/speedtest"
|
2022-09-01 01:09:44 +00:00
|
|
|
"tailscale.com/tailcfg"
|
2022-11-18 22:46:53 +00:00
|
|
|
"tailscale.com/types/netlogtype"
|
2022-04-25 18:30:39 +00:00
|
|
|
|
2022-02-19 05:13:32 +00:00
|
|
|
"cdr.dev/slog"
|
|
|
|
"github.com/coder/coder/agent/usershell"
|
2022-10-24 03:35:08 +00:00
|
|
|
"github.com/coder/coder/buildinfo"
|
2022-10-25 00:46:24 +00:00
|
|
|
"github.com/coder/coder/coderd/gitauth"
|
2022-09-23 19:51:04 +00:00
|
|
|
"github.com/coder/coder/codersdk"
|
2022-02-19 05:13:32 +00:00
|
|
|
"github.com/coder/coder/pty"
|
2022-09-01 01:09:44 +00:00
|
|
|
"github.com/coder/coder/tailnet"
|
2022-02-19 05:13:32 +00:00
|
|
|
"github.com/coder/retry"
|
2022-05-24 21:03:42 +00:00
|
|
|
)
|
2022-02-19 05:13:32 +00:00
|
|
|
|
2022-05-24 21:03:42 +00:00
|
|
|
const (
|
|
|
|
ProtocolReconnectingPTY = "reconnecting-pty"
|
|
|
|
ProtocolSSH = "ssh"
|
|
|
|
ProtocolDial = "dial"
|
2022-07-27 19:23:28 +00:00
|
|
|
|
|
|
|
// MagicSessionErrorCode indicates that something went wrong with the session, rather than the
|
|
|
|
// command just returning a nonzero exit code, and is chosen as an arbitrary, high number
|
|
|
|
// unlikely to shadow other exit codes, which are typically 1, 2, 3, etc.
|
|
|
|
MagicSessionErrorCode = 229
|
2022-02-19 05:13:32 +00:00
|
|
|
)
|
|
|
|
|
2022-04-29 22:30:10 +00:00
|
|
|
type Options struct {
|
2022-10-25 00:46:24 +00:00
|
|
|
Filesystem afero.Fs
|
2022-11-13 20:23:23 +00:00
|
|
|
TempDir string
|
2022-11-04 16:44:36 +00:00
|
|
|
ExchangeToken func(ctx context.Context) (string, error)
|
2022-10-24 03:35:08 +00:00
|
|
|
Client Client
|
|
|
|
ReconnectingPTYTimeout time.Duration
|
|
|
|
EnvironmentVariables map[string]string
|
|
|
|
Logger slog.Logger
|
2022-06-24 15:25:01 +00:00
|
|
|
}
|
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
type Client interface {
|
|
|
|
WorkspaceAgentMetadata(ctx context.Context) (codersdk.WorkspaceAgentMetadata, error)
|
|
|
|
ListenWorkspaceAgent(ctx context.Context) (net.Conn, error)
|
|
|
|
AgentReportStats(ctx context.Context, log slog.Logger, stats func() *codersdk.AgentStats) (io.Closer, error)
|
|
|
|
PostWorkspaceAgentAppHealth(ctx context.Context, req codersdk.PostWorkspaceAppHealthsRequest) error
|
|
|
|
PostWorkspaceAgentVersion(ctx context.Context, version string) error
|
|
|
|
}
|
2022-09-01 01:09:44 +00:00
|
|
|
|
|
|
|
func New(options Options) io.Closer {
|
2022-04-29 22:30:10 +00:00
|
|
|
if options.ReconnectingPTYTimeout == 0 {
|
|
|
|
options.ReconnectingPTYTimeout = 5 * time.Minute
|
|
|
|
}
|
2022-10-25 00:46:24 +00:00
|
|
|
if options.Filesystem == nil {
|
|
|
|
options.Filesystem = afero.NewOsFs()
|
|
|
|
}
|
2022-11-13 20:23:23 +00:00
|
|
|
if options.TempDir == "" {
|
|
|
|
options.TempDir = os.TempDir()
|
|
|
|
}
|
2022-11-04 16:44:36 +00:00
|
|
|
if options.ExchangeToken == nil {
|
|
|
|
options.ExchangeToken = func(ctx context.Context) (string, error) {
|
|
|
|
return "", nil
|
|
|
|
}
|
|
|
|
}
|
2022-02-19 05:13:32 +00:00
|
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
2022-12-13 19:28:07 +00:00
|
|
|
a := &agent{
|
2022-10-24 03:35:08 +00:00
|
|
|
reconnectingPTYTimeout: options.ReconnectingPTYTimeout,
|
|
|
|
logger: options.Logger,
|
|
|
|
closeCancel: cancelFunc,
|
|
|
|
closed: make(chan struct{}),
|
|
|
|
envVars: options.EnvironmentVariables,
|
|
|
|
client: options.Client,
|
|
|
|
exchangeToken: options.ExchangeToken,
|
2022-10-25 00:46:24 +00:00
|
|
|
filesystem: options.Filesystem,
|
2022-11-13 20:23:23 +00:00
|
|
|
tempDir: options.TempDir,
|
2022-02-19 05:13:32 +00:00
|
|
|
}
|
2022-12-13 19:28:07 +00:00
|
|
|
a.init(ctx)
|
|
|
|
return a
|
2022-02-19 05:13:32 +00:00
|
|
|
}
|
|
|
|
|
2022-03-25 19:48:08 +00:00
|
|
|
type agent struct {
|
2022-10-24 03:35:08 +00:00
|
|
|
logger slog.Logger
|
|
|
|
client Client
|
2022-11-04 16:44:36 +00:00
|
|
|
exchangeToken func(ctx context.Context) (string, error)
|
2022-10-25 00:46:24 +00:00
|
|
|
filesystem afero.Fs
|
2022-11-13 20:23:23 +00:00
|
|
|
tempDir string
|
2022-02-19 05:13:32 +00:00
|
|
|
|
2022-04-29 22:30:10 +00:00
|
|
|
reconnectingPTYs sync.Map
|
|
|
|
reconnectingPTYTimeout time.Duration
|
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
connCloseWait sync.WaitGroup
|
|
|
|
closeCancel context.CancelFunc
|
|
|
|
closeMutex sync.Mutex
|
|
|
|
closed chan struct{}
|
2022-02-19 05:13:32 +00:00
|
|
|
|
2022-04-30 16:40:30 +00:00
|
|
|
envVars map[string]string
|
|
|
|
// metadata is atomic because values can change after reconnection.
|
2022-11-04 16:44:36 +00:00
|
|
|
metadata atomic.Value
|
|
|
|
sessionToken atomic.Pointer[string]
|
|
|
|
sshServer *ssh.Server
|
2022-10-24 03:35:08 +00:00
|
|
|
|
|
|
|
network *tailnet.Conn
|
2022-02-19 05:13:32 +00:00
|
|
|
}
|
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
// runLoop attempts to start the agent in a retry loop.
|
|
|
|
// Coder may be offline temporarily, a connection issue
|
|
|
|
// may be happening, but regardless after the intermittent
|
|
|
|
// failure, you'll want the agent to reconnect.
|
|
|
|
func (a *agent) runLoop(ctx context.Context) {
|
|
|
|
for retrier := retry.New(100*time.Millisecond, 10*time.Second); retrier.Wait(ctx); {
|
|
|
|
a.logger.Info(ctx, "running loop")
|
|
|
|
err := a.run(ctx)
|
|
|
|
// Cancel after the run is complete to clean up any leaked resources!
|
|
|
|
if err == nil {
|
2022-03-22 19:17:50 +00:00
|
|
|
continue
|
|
|
|
}
|
2022-09-01 01:09:44 +00:00
|
|
|
if errors.Is(err, context.Canceled) {
|
|
|
|
return
|
|
|
|
}
|
2022-10-24 03:35:08 +00:00
|
|
|
if a.isClosed() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
a.logger.Info(ctx, "likely disconnected from coder", slog.Error(err))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
a.logger.Warn(ctx, "run exited with error", slog.Error(err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *agent) run(ctx context.Context) error {
|
|
|
|
// This allows the agent to refresh it's token if necessary.
|
|
|
|
// For instance identity this is required, since the instance
|
|
|
|
// may not have re-provisioned, but a new agent ID was created.
|
2022-11-04 16:44:36 +00:00
|
|
|
sessionToken, err := a.exchangeToken(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("exchange token: %w", err)
|
2022-10-24 03:35:08 +00:00
|
|
|
}
|
2022-11-04 16:44:36 +00:00
|
|
|
a.sessionToken.Store(&sessionToken)
|
2022-09-01 01:09:44 +00:00
|
|
|
|
2022-11-04 16:44:36 +00:00
|
|
|
err = a.client.PostWorkspaceAgentVersion(ctx, buildinfo.Version())
|
2022-10-24 03:35:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("update workspace agent version: %w", err)
|
2022-09-01 01:09:44 +00:00
|
|
|
}
|
2022-09-23 19:51:04 +00:00
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
metadata, err := a.client.WorkspaceAgentMetadata(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("fetch metadata: %w", err)
|
2022-09-23 19:51:04 +00:00
|
|
|
}
|
2022-11-14 11:48:44 +00:00
|
|
|
a.logger.Info(ctx, "fetched metadata")
|
2022-10-24 03:35:08 +00:00
|
|
|
oldMetadata := a.metadata.Swap(metadata)
|
|
|
|
|
|
|
|
// The startup script should only execute on the first run!
|
|
|
|
if oldMetadata == nil {
|
|
|
|
go func() {
|
|
|
|
err := a.runStartupScript(ctx, metadata.StartupScript)
|
|
|
|
if errors.Is(err, context.Canceled) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
a.logger.Warn(ctx, "agent script failed", slog.Error(err))
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2022-10-25 00:46:24 +00:00
|
|
|
if metadata.GitAuthConfigs > 0 {
|
|
|
|
err = gitauth.OverrideVSCodeConfigs(a.filesystem)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("override vscode configuration for git auth: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
// This automatically closes when the context ends!
|
|
|
|
appReporterCtx, appReporterCtxCancel := context.WithCancel(ctx)
|
|
|
|
defer appReporterCtxCancel()
|
|
|
|
go NewWorkspaceAppHealthReporter(
|
|
|
|
a.logger, metadata.Apps, a.client.PostWorkspaceAgentAppHealth)(appReporterCtx)
|
|
|
|
|
|
|
|
a.logger.Debug(ctx, "running tailnet with derpmap", slog.F("derpmap", metadata.DERPMap))
|
2022-09-01 01:09:44 +00:00
|
|
|
|
|
|
|
a.closeMutex.Lock()
|
2022-10-24 03:35:08 +00:00
|
|
|
network := a.network
|
|
|
|
a.closeMutex.Unlock()
|
2022-11-14 11:48:44 +00:00
|
|
|
if network == nil {
|
2022-10-24 03:35:08 +00:00
|
|
|
a.logger.Debug(ctx, "creating tailnet")
|
|
|
|
network, err = a.createTailnet(ctx, metadata.DERPMap)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("create tailnet: %w", err)
|
|
|
|
}
|
|
|
|
a.closeMutex.Lock()
|
2022-12-12 11:26:49 +00:00
|
|
|
// Re-check if agent was closed while initializing the network.
|
|
|
|
closed := a.isClosed()
|
|
|
|
if !closed {
|
|
|
|
a.network = network
|
|
|
|
}
|
2022-10-24 03:35:08 +00:00
|
|
|
a.closeMutex.Unlock()
|
2022-12-12 11:26:49 +00:00
|
|
|
if closed {
|
|
|
|
_ = network.Close()
|
|
|
|
return xerrors.New("agent is closed")
|
|
|
|
}
|
2022-12-14 16:45:46 +00:00
|
|
|
|
|
|
|
// Report statistics from the created network.
|
|
|
|
cl, err := a.client.AgentReportStats(ctx, a.logger, func() *codersdk.AgentStats {
|
|
|
|
stats := network.ExtractTrafficStats()
|
|
|
|
return convertAgentStats(stats)
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
a.logger.Error(ctx, "report stats", slog.Error(err))
|
|
|
|
} else {
|
|
|
|
if err = a.trackConnGoroutine(func() {
|
|
|
|
// This is OK because the agent never re-creates the tailnet
|
|
|
|
// and the only shutdown indicator is agent.Close().
|
|
|
|
<-a.closed
|
|
|
|
_ = cl.Close()
|
|
|
|
}); err != nil {
|
|
|
|
a.logger.Debug(ctx, "report stats goroutine", slog.Error(err))
|
|
|
|
_ = cl.Close()
|
|
|
|
}
|
|
|
|
}
|
2022-10-24 03:35:08 +00:00
|
|
|
} else {
|
|
|
|
// Update the DERP map!
|
|
|
|
network.SetDERPMap(metadata.DERPMap)
|
2022-09-01 01:09:44 +00:00
|
|
|
}
|
2022-10-24 03:35:08 +00:00
|
|
|
|
|
|
|
a.logger.Debug(ctx, "running coordinator")
|
|
|
|
err = a.runCoordinator(ctx, network)
|
|
|
|
if err != nil {
|
|
|
|
a.logger.Debug(ctx, "coordinator exited", slog.Error(err))
|
|
|
|
return xerrors.Errorf("run coordinator: %w", err)
|
2022-09-01 01:09:44 +00:00
|
|
|
}
|
2022-10-24 03:35:08 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-12-02 14:24:40 +00:00
|
|
|
func (a *agent) trackConnGoroutine(fn func()) error {
|
|
|
|
a.closeMutex.Lock()
|
|
|
|
defer a.closeMutex.Unlock()
|
|
|
|
if a.isClosed() {
|
|
|
|
return xerrors.New("track conn goroutine: agent is closed")
|
|
|
|
}
|
|
|
|
a.connCloseWait.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer a.connCloseWait.Done()
|
|
|
|
fn()
|
|
|
|
}()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-12-05 22:18:23 +00:00
|
|
|
func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_ *tailnet.Conn, err error) {
|
|
|
|
network, err := tailnet.NewConn(&tailnet.Options{
|
2022-11-18 22:46:53 +00:00
|
|
|
Addresses: []netip.Prefix{netip.PrefixFrom(codersdk.TailnetIP, 128)},
|
|
|
|
DERPMap: derpMap,
|
|
|
|
Logger: a.logger.Named("tailnet"),
|
|
|
|
EnableTrafficStats: true,
|
2022-09-01 01:09:44 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
2022-10-24 03:35:08 +00:00
|
|
|
return nil, xerrors.Errorf("create tailnet: %w", err)
|
2022-09-01 01:09:44 +00:00
|
|
|
}
|
2022-12-02 14:24:40 +00:00
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
|
|
|
network.Close()
|
|
|
|
}
|
|
|
|
}()
|
2022-09-01 01:09:44 +00:00
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
sshListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetSSHPort))
|
2022-09-01 01:09:44 +00:00
|
|
|
if err != nil {
|
2022-10-24 03:35:08 +00:00
|
|
|
return nil, xerrors.Errorf("listen on the ssh port: %w", err)
|
2022-09-01 01:09:44 +00:00
|
|
|
}
|
2022-12-02 14:24:40 +00:00
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
|
|
|
_ = sshListener.Close()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
if err = a.trackConnGoroutine(func() {
|
2022-09-01 01:09:44 +00:00
|
|
|
for {
|
|
|
|
conn, err := sshListener.Accept()
|
|
|
|
if err != nil {
|
2022-04-25 18:30:39 +00:00
|
|
|
return
|
|
|
|
}
|
2023-01-10 04:23:17 +00:00
|
|
|
closed := make(chan struct{})
|
|
|
|
_ = a.trackConnGoroutine(func() {
|
|
|
|
select {
|
|
|
|
case <-network.Closed():
|
|
|
|
case <-closed:
|
|
|
|
}
|
|
|
|
_ = conn.Close()
|
|
|
|
})
|
|
|
|
_ = a.trackConnGoroutine(func() {
|
|
|
|
defer close(closed)
|
|
|
|
a.sshServer.HandleConn(conn)
|
|
|
|
})
|
2022-09-01 01:09:44 +00:00
|
|
|
}
|
2022-12-02 14:24:40 +00:00
|
|
|
}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-10-06 12:38:22 +00:00
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
reconnectingPTYListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetReconnectingPTYPort))
|
2022-09-01 01:09:44 +00:00
|
|
|
if err != nil {
|
2022-10-24 03:35:08 +00:00
|
|
|
return nil, xerrors.Errorf("listen for reconnecting pty: %w", err)
|
2022-09-01 01:09:44 +00:00
|
|
|
}
|
2022-12-02 14:24:40 +00:00
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
|
|
|
_ = reconnectingPTYListener.Close()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
if err = a.trackConnGoroutine(func() {
|
2022-12-13 19:28:07 +00:00
|
|
|
logger := a.logger.Named("reconnecting-pty")
|
|
|
|
|
2022-09-01 01:09:44 +00:00
|
|
|
for {
|
|
|
|
conn, err := reconnectingPTYListener.Accept()
|
2022-04-25 18:30:39 +00:00
|
|
|
if err != nil {
|
2022-12-13 19:28:07 +00:00
|
|
|
logger.Debug(ctx, "accept pty failed", slog.Error(err))
|
2022-09-01 01:09:44 +00:00
|
|
|
return
|
2022-04-25 18:30:39 +00:00
|
|
|
}
|
2022-09-01 01:09:44 +00:00
|
|
|
// This cannot use a JSON decoder, since that can
|
|
|
|
// buffer additional data that is required for the PTY.
|
|
|
|
rawLen := make([]byte, 2)
|
|
|
|
_, err = conn.Read(rawLen)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
length := binary.LittleEndian.Uint16(rawLen)
|
|
|
|
data := make([]byte, length)
|
|
|
|
_, err = conn.Read(data)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2022-09-23 19:51:04 +00:00
|
|
|
var msg codersdk.ReconnectingPTYInit
|
2022-09-01 01:09:44 +00:00
|
|
|
err = json.Unmarshal(data, &msg)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2022-12-13 19:28:07 +00:00
|
|
|
go func() {
|
|
|
|
_ = a.handleReconnectingPTY(ctx, logger, msg, conn)
|
|
|
|
}()
|
2022-09-01 01:09:44 +00:00
|
|
|
}
|
2022-12-02 14:24:40 +00:00
|
|
|
}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-10-06 12:38:22 +00:00
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
speedtestListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetSpeedtestPort))
|
2022-09-05 22:15:49 +00:00
|
|
|
if err != nil {
|
2022-10-24 03:35:08 +00:00
|
|
|
return nil, xerrors.Errorf("listen for speedtest: %w", err)
|
2022-09-05 22:15:49 +00:00
|
|
|
}
|
2022-12-02 14:24:40 +00:00
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
|
|
|
_ = speedtestListener.Close()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
if err = a.trackConnGoroutine(func() {
|
2022-09-05 22:15:49 +00:00
|
|
|
for {
|
|
|
|
conn, err := speedtestListener.Accept()
|
|
|
|
if err != nil {
|
|
|
|
a.logger.Debug(ctx, "speedtest listener failed", slog.Error(err))
|
|
|
|
return
|
|
|
|
}
|
2022-12-02 14:24:40 +00:00
|
|
|
if err = a.trackConnGoroutine(func() {
|
2022-09-05 22:15:49 +00:00
|
|
|
_ = speedtest.ServeConn(conn)
|
2022-12-02 14:24:40 +00:00
|
|
|
}); err != nil {
|
|
|
|
a.logger.Debug(ctx, "speedtest listener failed", slog.Error(err))
|
|
|
|
_ = conn.Close()
|
|
|
|
return
|
|
|
|
}
|
2022-09-05 22:15:49 +00:00
|
|
|
}
|
2022-12-02 14:24:40 +00:00
|
|
|
}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-10-06 12:38:22 +00:00
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
statisticsListener, err := network.Listen("tcp", ":"+strconv.Itoa(codersdk.TailnetStatisticsPort))
|
2022-10-06 12:38:22 +00:00
|
|
|
if err != nil {
|
2022-10-24 03:35:08 +00:00
|
|
|
return nil, xerrors.Errorf("listen for statistics: %w", err)
|
2022-10-06 12:38:22 +00:00
|
|
|
}
|
2022-12-02 14:24:40 +00:00
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
|
|
|
_ = statisticsListener.Close()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
if err = a.trackConnGoroutine(func() {
|
2022-10-06 12:38:22 +00:00
|
|
|
defer statisticsListener.Close()
|
|
|
|
server := &http.Server{
|
|
|
|
Handler: a.statisticsHandler(),
|
|
|
|
ReadTimeout: 20 * time.Second,
|
|
|
|
ReadHeaderTimeout: 20 * time.Second,
|
|
|
|
WriteTimeout: 20 * time.Second,
|
|
|
|
ErrorLog: slog.Stdlib(ctx, a.logger.Named("statistics_http_server"), slog.LevelInfo),
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
<-ctx.Done()
|
|
|
|
_ = server.Close()
|
|
|
|
}()
|
|
|
|
|
2022-12-02 14:24:40 +00:00
|
|
|
err := server.Serve(statisticsListener)
|
2022-10-06 12:38:22 +00:00
|
|
|
if err != nil && !xerrors.Is(err, http.ErrServerClosed) && !strings.Contains(err.Error(), "use of closed network connection") {
|
|
|
|
a.logger.Critical(ctx, "serve statistics HTTP server", slog.Error(err))
|
|
|
|
}
|
2022-12-02 14:24:40 +00:00
|
|
|
}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-09-01 01:09:44 +00:00
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
return network, nil
|
2022-09-23 15:08:13 +00:00
|
|
|
}
|
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
// runCoordinator runs a coordinator and returns whether a reconnect
|
|
|
|
// should occur.
|
|
|
|
func (a *agent) runCoordinator(ctx context.Context, network *tailnet.Conn) error {
|
2023-01-23 20:05:29 +00:00
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
defer cancel()
|
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
coordinator, err := a.client.ListenWorkspaceAgent(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2022-09-01 01:09:44 +00:00
|
|
|
}
|
2022-10-24 03:35:08 +00:00
|
|
|
defer coordinator.Close()
|
2022-11-14 11:48:44 +00:00
|
|
|
a.logger.Info(ctx, "connected to coordination server")
|
2022-10-24 03:35:08 +00:00
|
|
|
sendNodes, errChan := tailnet.ServeCoordinator(coordinator, network.UpdateNodes)
|
|
|
|
network.SetNodeCallback(sendNodes)
|
2022-09-01 01:09:44 +00:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2022-10-24 03:35:08 +00:00
|
|
|
return ctx.Err()
|
2022-09-01 01:09:44 +00:00
|
|
|
case err := <-errChan:
|
2022-10-24 03:35:08 +00:00
|
|
|
return err
|
2022-04-25 18:30:39 +00:00
|
|
|
}
|
2022-09-01 01:09:44 +00:00
|
|
|
}
|
2022-03-22 19:17:50 +00:00
|
|
|
|
2022-06-06 19:20:25 +00:00
|
|
|
func (a *agent) runStartupScript(ctx context.Context, script string) error {
|
2022-04-25 18:30:39 +00:00
|
|
|
if script == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-11-13 20:23:23 +00:00
|
|
|
a.logger.Info(ctx, "running startup script", slog.F("script", script))
|
|
|
|
writer, err := a.filesystem.OpenFile(filepath.Join(a.tempDir, "coder-startup-script.log"), os.O_CREATE|os.O_RDWR, 0o600)
|
2022-04-25 18:30:39 +00:00
|
|
|
if err != nil {
|
2022-05-02 16:36:51 +00:00
|
|
|
return xerrors.Errorf("open startup script log file: %w", err)
|
2022-04-25 18:30:39 +00:00
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
_ = writer.Close()
|
|
|
|
}()
|
2022-06-06 19:20:25 +00:00
|
|
|
cmd, err := a.createCommand(ctx, script, nil)
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("create command: %w", err)
|
2022-04-25 18:30:39 +00:00
|
|
|
}
|
|
|
|
cmd.Stdout = writer
|
|
|
|
cmd.Stderr = writer
|
|
|
|
err = cmd.Run()
|
|
|
|
if err != nil {
|
2022-05-24 21:03:42 +00:00
|
|
|
// cmd.Run does not return a context canceled error, it returns "signal: killed".
|
|
|
|
if ctx.Err() != nil {
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
|
2022-04-25 18:30:39 +00:00
|
|
|
return xerrors.Errorf("run: %w", err)
|
|
|
|
}
|
2022-05-24 21:03:42 +00:00
|
|
|
|
2022-04-25 18:30:39 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-03-30 22:59:54 +00:00
|
|
|
func (a *agent) init(ctx context.Context) {
|
2022-08-12 12:01:00 +00:00
|
|
|
a.logger.Info(ctx, "generating host key")
|
2022-02-19 05:13:32 +00:00
|
|
|
// Clients' should ignore the host key when connecting.
|
|
|
|
// The agent needs to authenticate with coderd to SSH,
|
|
|
|
// so SSH authentication doesn't improve security.
|
|
|
|
randomHostKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
randomSigner, err := gossh.NewSignerFromKey(randomHostKey)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2023-01-06 07:52:19 +00:00
|
|
|
|
2022-04-18 22:40:25 +00:00
|
|
|
sshLogger := a.logger.Named("ssh-server")
|
2022-02-19 05:13:32 +00:00
|
|
|
forwardHandler := &ssh.ForwardedTCPHandler{}
|
2023-01-06 07:52:19 +00:00
|
|
|
unixForwardHandler := &forwardedUnixHandler{log: a.logger}
|
|
|
|
|
2022-03-30 22:59:54 +00:00
|
|
|
a.sshServer = &ssh.Server{
|
2022-04-12 00:17:18 +00:00
|
|
|
ChannelHandlers: map[string]ssh.ChannelHandler{
|
2023-01-06 07:52:19 +00:00
|
|
|
"direct-tcpip": ssh.DirectTCPIPHandler,
|
|
|
|
"direct-streamlocal@openssh.com": directStreamLocalHandler,
|
|
|
|
"session": ssh.DefaultSessionHandler,
|
2022-04-12 00:17:18 +00:00
|
|
|
},
|
2022-02-19 05:13:32 +00:00
|
|
|
ConnectionFailedCallback: func(conn net.Conn, err error) {
|
|
|
|
sshLogger.Info(ctx, "ssh connection ended", slog.Error(err))
|
|
|
|
},
|
|
|
|
Handler: func(session ssh.Session) {
|
2022-03-30 22:59:54 +00:00
|
|
|
err := a.handleSSHSession(session)
|
2022-07-27 19:23:28 +00:00
|
|
|
var exitError *exec.ExitError
|
|
|
|
if xerrors.As(err, &exitError) {
|
|
|
|
a.logger.Debug(ctx, "ssh session returned", slog.Error(exitError))
|
|
|
|
_ = session.Exit(exitError.ExitCode())
|
|
|
|
return
|
|
|
|
}
|
2022-02-19 05:13:32 +00:00
|
|
|
if err != nil {
|
2022-04-18 22:40:25 +00:00
|
|
|
a.logger.Warn(ctx, "ssh session failed", slog.Error(err))
|
2022-07-27 19:23:28 +00:00
|
|
|
// This exit code is designed to be unlikely to be confused for a legit exit code
|
|
|
|
// from the process.
|
|
|
|
_ = session.Exit(MagicSessionErrorCode)
|
2022-02-19 05:13:32 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
},
|
|
|
|
HostSigners: []ssh.Signer{randomSigner},
|
|
|
|
LocalPortForwardingCallback: func(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
|
|
|
|
// Allow local port forwarding all!
|
|
|
|
sshLogger.Debug(ctx, "local port forward",
|
|
|
|
slog.F("destination-host", destinationHost),
|
|
|
|
slog.F("destination-port", destinationPort))
|
|
|
|
return true
|
|
|
|
},
|
|
|
|
PtyCallback: func(ctx ssh.Context, pty ssh.Pty) bool {
|
|
|
|
return true
|
|
|
|
},
|
|
|
|
ReversePortForwardingCallback: func(ctx ssh.Context, bindHost string, bindPort uint32) bool {
|
|
|
|
// Allow reverse port forwarding all!
|
|
|
|
sshLogger.Debug(ctx, "local port forward",
|
|
|
|
slog.F("bind-host", bindHost),
|
|
|
|
slog.F("bind-port", bindPort))
|
|
|
|
return true
|
|
|
|
},
|
|
|
|
RequestHandlers: map[string]ssh.RequestHandler{
|
2023-01-06 07:52:19 +00:00
|
|
|
"tcpip-forward": forwardHandler.HandleSSHRequest,
|
|
|
|
"cancel-tcpip-forward": forwardHandler.HandleSSHRequest,
|
|
|
|
"streamlocal-forward@openssh.com": unixForwardHandler.HandleSSHRequest,
|
|
|
|
"cancel-streamlocal-forward@openssh.com": unixForwardHandler.HandleSSHRequest,
|
2022-02-19 05:13:32 +00:00
|
|
|
},
|
|
|
|
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
|
|
|
|
return &gossh.ServerConfig{
|
|
|
|
NoClientAuth: true,
|
|
|
|
}
|
|
|
|
},
|
2022-04-12 00:17:18 +00:00
|
|
|
SubsystemHandlers: map[string]ssh.SubsystemHandler{
|
|
|
|
"sftp": func(session ssh.Session) {
|
2022-11-13 21:22:50 +00:00
|
|
|
ctx := session.Context()
|
|
|
|
|
|
|
|
// Typically sftp sessions don't request a TTY, but if they do,
|
|
|
|
// we must ensure the gliderlabs/ssh CRLF emulation is disabled.
|
|
|
|
// Otherwise sftp will be broken. This can happen if a user sets
|
|
|
|
// `RequestTTY force` in their SSH config.
|
2022-09-12 16:27:51 +00:00
|
|
|
session.DisablePTYEmulation()
|
|
|
|
|
2022-10-21 14:54:06 +00:00
|
|
|
var opts []sftp.ServerOption
|
|
|
|
// Change current working directory to the users home
|
|
|
|
// directory so that SFTP connections land there.
|
2022-11-24 12:22:20 +00:00
|
|
|
homedir, err := userHomeDir()
|
2022-10-21 14:54:06 +00:00
|
|
|
if err != nil {
|
2022-11-24 12:22:20 +00:00
|
|
|
sshLogger.Warn(ctx, "get sftp working directory failed, unable to get home dir", slog.Error(err))
|
2022-10-21 14:54:06 +00:00
|
|
|
} else {
|
2022-11-24 12:22:20 +00:00
|
|
|
opts = append(opts, sftp.WithServerWorkingDirectory(homedir))
|
2022-10-21 14:54:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
server, err := sftp.NewServer(session, opts...)
|
2022-04-12 00:17:18 +00:00
|
|
|
if err != nil {
|
2022-11-13 21:22:50 +00:00
|
|
|
sshLogger.Debug(ctx, "initialize sftp server", slog.Error(err))
|
2022-04-12 00:17:18 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer server.Close()
|
2022-11-13 21:22:50 +00:00
|
|
|
|
2022-04-12 00:17:18 +00:00
|
|
|
err = server.Serve()
|
|
|
|
if errors.Is(err, io.EOF) {
|
2022-11-13 21:22:50 +00:00
|
|
|
// Unless we call `session.Exit(0)` here, the client won't
|
|
|
|
// receive `exit-status` because `(*sftp.Server).Close()`
|
|
|
|
// calls `Close()` on the underlying connection (session),
|
|
|
|
// which actually calls `channel.Close()` because it isn't
|
|
|
|
// wrapped. This causes sftp clients to receive a non-zero
|
|
|
|
// exit code. Typically sftp clients don't echo this exit
|
|
|
|
// code but `scp` on macOS does (when using the default
|
|
|
|
// SFTP backend).
|
|
|
|
_ = session.Exit(0)
|
2022-04-12 00:17:18 +00:00
|
|
|
return
|
|
|
|
}
|
2022-11-13 21:22:50 +00:00
|
|
|
sshLogger.Warn(ctx, "sftp server closed with error", slog.Error(err))
|
|
|
|
_ = session.Exit(1)
|
2022-04-12 00:17:18 +00:00
|
|
|
},
|
|
|
|
},
|
2022-02-19 05:13:32 +00:00
|
|
|
}
|
|
|
|
|
2022-10-24 03:35:08 +00:00
|
|
|
go a.runLoop(ctx)
|
2022-02-19 05:13:32 +00:00
|
|
|
}
|
|
|
|
|
2022-11-18 22:46:53 +00:00
|
|
|
func convertAgentStats(counts map[netlogtype.Connection]netlogtype.Counts) *codersdk.AgentStats {
|
|
|
|
stats := &codersdk.AgentStats{
|
|
|
|
ConnsByProto: map[string]int64{},
|
|
|
|
NumConns: int64(len(counts)),
|
|
|
|
}
|
|
|
|
|
|
|
|
for conn, count := range counts {
|
|
|
|
stats.ConnsByProto[conn.Proto.String()]++
|
|
|
|
stats.RxPackets += int64(count.RxPackets)
|
|
|
|
stats.RxBytes += int64(count.RxBytes)
|
|
|
|
stats.TxPackets += int64(count.TxPackets)
|
|
|
|
stats.TxBytes += int64(count.TxBytes)
|
|
|
|
}
|
|
|
|
|
|
|
|
return stats
|
|
|
|
}
|
|
|
|
|
2022-04-29 22:30:10 +00:00
|
|
|
// createCommand processes raw command input with OpenSSH-like behavior.
|
|
|
|
// If the rawCommand provided is empty, it will default to the users shell.
|
|
|
|
// This injects environment variables specified by the user at launch too.
|
|
|
|
func (a *agent) createCommand(ctx context.Context, rawCommand string, env []string) (*exec.Cmd, error) {
|
2022-03-30 22:59:54 +00:00
|
|
|
currentUser, err := user.Current()
|
|
|
|
if err != nil {
|
2022-04-29 22:30:10 +00:00
|
|
|
return nil, xerrors.Errorf("get current user: %w", err)
|
2022-02-19 05:13:32 +00:00
|
|
|
}
|
2022-03-30 22:59:54 +00:00
|
|
|
username := currentUser.Username
|
2022-02-19 05:13:32 +00:00
|
|
|
|
2022-04-12 00:17:18 +00:00
|
|
|
shell, err := usershell.Get(username)
|
|
|
|
if err != nil {
|
2022-04-29 22:30:10 +00:00
|
|
|
return nil, xerrors.Errorf("get user shell: %w", err)
|
2022-04-12 00:17:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-30 16:40:30 +00:00
|
|
|
rawMetadata := a.metadata.Load()
|
|
|
|
if rawMetadata == nil {
|
|
|
|
return nil, xerrors.Errorf("no metadata was provided: %w", err)
|
|
|
|
}
|
2022-09-23 19:51:04 +00:00
|
|
|
metadata, valid := rawMetadata.(codersdk.WorkspaceAgentMetadata)
|
2022-04-30 16:40:30 +00:00
|
|
|
if !valid {
|
|
|
|
return nil, xerrors.Errorf("metadata is the wrong type: %T", metadata)
|
|
|
|
}
|
|
|
|
|
2022-11-14 12:01:22 +00:00
|
|
|
// OpenSSH executes all commands with the users current shell.
|
|
|
|
// We replicate that behavior for IDE support.
|
|
|
|
caller := "-c"
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
caller = "/c"
|
|
|
|
}
|
|
|
|
args := []string{caller, rawCommand}
|
|
|
|
|
2022-02-19 05:13:32 +00:00
|
|
|
// gliderlabs/ssh returns a command slice of zero
|
|
|
|
// when a shell is requested.
|
2022-11-14 12:01:22 +00:00
|
|
|
if len(rawCommand) == 0 {
|
|
|
|
args = []string{}
|
2022-06-17 05:54:45 +00:00
|
|
|
if runtime.GOOS != "windows" {
|
|
|
|
// On Linux and macOS, we should start a login
|
|
|
|
// shell to consume juicy environment variables!
|
2022-11-14 12:01:22 +00:00
|
|
|
args = append(args, "-l")
|
2022-06-17 05:54:45 +00:00
|
|
|
}
|
2022-02-19 05:13:32 +00:00
|
|
|
}
|
|
|
|
|
2022-11-14 12:01:22 +00:00
|
|
|
cmd := exec.CommandContext(ctx, shell, args...)
|
2022-05-02 15:27:34 +00:00
|
|
|
cmd.Dir = metadata.Directory
|
|
|
|
if cmd.Dir == "" {
|
2022-11-24 12:22:20 +00:00
|
|
|
// Default to user home if a directory is not set.
|
|
|
|
homedir, err := userHomeDir()
|
|
|
|
if err != nil {
|
|
|
|
return nil, xerrors.Errorf("get home dir: %w", err)
|
|
|
|
}
|
|
|
|
cmd.Dir = homedir
|
2022-05-02 15:27:34 +00:00
|
|
|
}
|
2022-04-29 22:30:10 +00:00
|
|
|
cmd.Env = append(os.Environ(), env...)
|
2022-04-26 01:03:54 +00:00
|
|
|
executablePath, err := os.Executable()
|
|
|
|
if err != nil {
|
2022-04-29 22:30:10 +00:00
|
|
|
return nil, xerrors.Errorf("getting os executable: %w", err)
|
2022-04-26 01:03:54 +00:00
|
|
|
}
|
2022-08-23 11:29:01 +00:00
|
|
|
// Set environment variables reliable detection of being inside a
|
|
|
|
// Coder workspace.
|
|
|
|
cmd.Env = append(cmd.Env, "CODER=true")
|
2022-05-26 23:01:47 +00:00
|
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", username))
|
2022-04-26 01:03:54 +00:00
|
|
|
// Git on Windows resolves with UNIX-style paths.
|
|
|
|
// If using backslashes, it's unable to find the executable.
|
2022-05-26 17:59:41 +00:00
|
|
|
unixExecutablePath := strings.ReplaceAll(executablePath, "\\", "/")
|
|
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_SSH_COMMAND=%s gitssh --`, unixExecutablePath))
|
2022-04-25 18:30:39 +00:00
|
|
|
|
2022-11-04 16:44:36 +00:00
|
|
|
// Specific Coder subcommands require the agent token exposed!
|
|
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("CODER_AGENT_TOKEN=%s", *a.sessionToken.Load()))
|
|
|
|
|
2022-08-23 18:19:57 +00:00
|
|
|
// Set SSH connection environment variables (these are also set by OpenSSH
|
|
|
|
// and thus expected to be present by SSH clients). Since the agent does
|
|
|
|
// networking in-memory, trying to provide accurate values here would be
|
|
|
|
// nonsensical. For now, we hard code these values so that they're present.
|
|
|
|
srcAddr, srcPort := "0.0.0.0", "0"
|
|
|
|
dstAddr, dstPort := "0.0.0.0", "0"
|
|
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_CLIENT=%s %s %s", srcAddr, srcPort, dstPort))
|
|
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_CONNECTION=%s %s %s %s", srcAddr, srcPort, dstAddr, dstPort))
|
|
|
|
|
2022-11-04 04:45:43 +00:00
|
|
|
// This adds the ports dialog to code-server that enables
|
|
|
|
// proxying a port dynamically.
|
|
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("VSCODE_PROXY_URI=%s", metadata.VSCodePortProxyURI))
|
|
|
|
|
2022-11-03 16:04:27 +00:00
|
|
|
// Hide Coder message on code-server's "Getting Started" page
|
|
|
|
cmd.Env = append(cmd.Env, "CS_DISABLE_GETTING_STARTED_OVERRIDE=true")
|
|
|
|
|
2022-04-25 18:30:39 +00:00
|
|
|
// Load environment variables passed via the agent.
|
2022-04-26 01:03:54 +00:00
|
|
|
// These should override all variables we manually specify.
|
2022-06-24 15:25:01 +00:00
|
|
|
for envKey, value := range metadata.EnvironmentVariables {
|
2022-06-17 05:54:45 +00:00
|
|
|
// Expanding environment variables allows for customization
|
2022-08-01 13:29:52 +00:00
|
|
|
// of the $PATH, among other variables. Customers can prepend
|
2022-06-17 05:54:45 +00:00
|
|
|
// or append to the $PATH, so allowing expand is required!
|
2022-06-24 15:25:01 +00:00
|
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", envKey, os.ExpandEnv(value)))
|
2022-04-30 16:40:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Agent-level environment variables should take over all!
|
|
|
|
// This is used for setting agent-specific variables like "CODER_AGENT_TOKEN".
|
2022-06-24 15:25:01 +00:00
|
|
|
for envKey, value := range a.envVars {
|
|
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", envKey, value))
|
2022-04-25 18:30:39 +00:00
|
|
|
}
|
2022-04-30 16:40:30 +00:00
|
|
|
|
2022-04-29 22:30:10 +00:00
|
|
|
return cmd, nil
|
|
|
|
}
|
|
|
|
|
2022-07-27 19:23:28 +00:00
|
|
|
func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
|
2022-09-12 16:27:51 +00:00
|
|
|
ctx := session.Context()
|
|
|
|
cmd, err := a.createCommand(ctx, session.RawCommand(), session.Environ())
|
2022-04-29 22:30:10 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-04-25 18:30:39 +00:00
|
|
|
|
2022-05-25 18:28:10 +00:00
|
|
|
if ssh.AgentRequested(session) {
|
|
|
|
l, err := ssh.NewAgentListener()
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("new agent listener: %w", err)
|
|
|
|
}
|
|
|
|
defer l.Close()
|
|
|
|
go ssh.ForwardAgentConnections(l, session)
|
|
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", "SSH_AUTH_SOCK", l.Addr().String()))
|
|
|
|
}
|
|
|
|
|
2022-02-19 05:13:32 +00:00
|
|
|
sshPty, windowSize, isPty := session.Pty()
|
|
|
|
if isPty {
|
2022-09-12 16:27:51 +00:00
|
|
|
// Disable minimal PTY emulation set by gliderlabs/ssh (NL-to-CRNL).
|
|
|
|
// See https://github.com/coder/coder/issues/3371.
|
|
|
|
session.DisablePTYEmulation()
|
|
|
|
|
2022-11-24 12:22:20 +00:00
|
|
|
if !isQuietLogin(session.RawCommand()) {
|
|
|
|
metadata, ok := a.metadata.Load().(codersdk.WorkspaceAgentMetadata)
|
|
|
|
if ok {
|
|
|
|
err = showMOTD(session, metadata.MOTDFile)
|
|
|
|
if err != nil {
|
|
|
|
a.logger.Error(ctx, "show MOTD", slog.Error(err))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
a.logger.Warn(ctx, "metadata lookup failed, unable to show MOTD")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-19 05:13:32 +00:00
|
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", sshPty.Term))
|
2022-08-23 18:19:57 +00:00
|
|
|
|
|
|
|
// The pty package sets `SSH_TTY` on supported platforms.
|
2022-09-12 16:27:51 +00:00
|
|
|
ptty, process, err := pty.Start(cmd, pty.WithPTYOption(
|
|
|
|
pty.WithSSHRequest(sshPty),
|
|
|
|
pty.WithLogger(slog.Stdlib(ctx, a.logger, slog.LevelInfo)),
|
|
|
|
))
|
2022-02-19 05:13:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("start command: %w", err)
|
|
|
|
}
|
2022-07-27 19:23:28 +00:00
|
|
|
defer func() {
|
|
|
|
closeErr := ptty.Close()
|
|
|
|
if closeErr != nil {
|
2022-09-12 16:27:51 +00:00
|
|
|
a.logger.Warn(ctx, "failed to close tty", slog.Error(closeErr))
|
2022-07-27 19:23:28 +00:00
|
|
|
if retErr == nil {
|
|
|
|
retErr = closeErr
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
2022-02-19 05:13:32 +00:00
|
|
|
go func() {
|
|
|
|
for win := range windowSize {
|
2022-07-27 19:23:28 +00:00
|
|
|
resizeErr := ptty.Resize(uint16(win.Height), uint16(win.Width))
|
|
|
|
if resizeErr != nil {
|
2022-09-12 16:27:51 +00:00
|
|
|
a.logger.Warn(ctx, "failed to resize tty", slog.Error(resizeErr))
|
2022-02-19 05:13:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
go func() {
|
|
|
|
_, _ = io.Copy(ptty.Input(), session)
|
|
|
|
}()
|
|
|
|
go func() {
|
|
|
|
_, _ = io.Copy(session, ptty.Output())
|
|
|
|
}()
|
2022-07-27 19:23:28 +00:00
|
|
|
err = process.Wait()
|
|
|
|
var exitErr *exec.ExitError
|
|
|
|
// ExitErrors just mean the command we run returned a non-zero exit code, which is normal
|
|
|
|
// and not something to be concerned about. But, if it's something else, we should log it.
|
|
|
|
if err != nil && !xerrors.As(err, &exitErr) {
|
2022-09-12 16:27:51 +00:00
|
|
|
a.logger.Warn(ctx, "wait error", slog.Error(err))
|
2022-07-27 19:23:28 +00:00
|
|
|
}
|
|
|
|
return err
|
2022-02-19 05:13:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cmd.Stdout = session
|
2022-04-12 00:17:18 +00:00
|
|
|
cmd.Stderr = session.Stderr()
|
2022-02-19 05:13:32 +00:00
|
|
|
// This blocks forever until stdin is received if we don't
|
|
|
|
// use StdinPipe. It's unknown what causes this.
|
|
|
|
stdinPipe, err := cmd.StdinPipe()
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("create stdin pipe: %w", err)
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
_, _ = io.Copy(stdinPipe, session)
|
2022-06-27 16:41:53 +00:00
|
|
|
_ = stdinPipe.Close()
|
2022-02-19 05:13:32 +00:00
|
|
|
}()
|
|
|
|
err = cmd.Start()
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("start: %w", err)
|
|
|
|
}
|
2022-04-12 00:17:18 +00:00
|
|
|
return cmd.Wait()
|
2022-02-19 05:13:32 +00:00
|
|
|
}
|
|
|
|
|
2022-12-13 19:28:07 +00:00
|
|
|
func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, msg codersdk.ReconnectingPTYInit, conn net.Conn) (retErr error) {
|
2022-04-29 22:30:10 +00:00
|
|
|
defer conn.Close()
|
|
|
|
|
2022-11-17 16:57:15 +00:00
|
|
|
connectionID := uuid.NewString()
|
2022-12-13 19:28:07 +00:00
|
|
|
logger = logger.With(slog.F("id", msg.ID), slog.F("connection_id", connectionID))
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if err := retErr; err != nil {
|
|
|
|
a.closeMutex.Lock()
|
|
|
|
closed := a.isClosed()
|
|
|
|
a.closeMutex.Unlock()
|
|
|
|
|
|
|
|
// If the agent is closed, we don't want to
|
|
|
|
// log this as an error since it's expected.
|
|
|
|
if closed {
|
|
|
|
logger.Debug(ctx, "session error after agent close", slog.Error(err))
|
|
|
|
} else {
|
|
|
|
logger.Error(ctx, "session error", slog.Error(err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
logger.Debug(ctx, "session closed")
|
|
|
|
}()
|
|
|
|
|
2022-04-29 22:30:10 +00:00
|
|
|
var rpty *reconnectingPTY
|
2022-09-01 01:09:44 +00:00
|
|
|
rawRPTY, ok := a.reconnectingPTYs.Load(msg.ID)
|
2022-04-29 22:30:10 +00:00
|
|
|
if ok {
|
2022-12-13 19:28:07 +00:00
|
|
|
logger.Debug(ctx, "connecting to existing session")
|
2022-04-29 22:30:10 +00:00
|
|
|
rpty, ok = rawRPTY.(*reconnectingPTY)
|
|
|
|
if !ok {
|
2022-12-13 19:28:07 +00:00
|
|
|
return xerrors.Errorf("found invalid type in reconnecting pty map: %T", rawRPTY)
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
} else {
|
2022-12-13 19:28:07 +00:00
|
|
|
logger.Debug(ctx, "creating new session")
|
|
|
|
|
2022-04-29 22:30:10 +00:00
|
|
|
// Empty command will default to the users shell!
|
2022-09-01 01:09:44 +00:00
|
|
|
cmd, err := a.createCommand(ctx, msg.Command, nil)
|
2022-04-29 22:30:10 +00:00
|
|
|
if err != nil {
|
2022-12-13 19:28:07 +00:00
|
|
|
return xerrors.Errorf("create command: %w", err)
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
cmd.Env = append(cmd.Env, "TERM=xterm-256color")
|
|
|
|
|
2022-09-05 13:45:10 +00:00
|
|
|
// Default to buffer 64KiB.
|
|
|
|
circularBuffer, err := circbuf.NewBuffer(64 << 10)
|
2022-04-29 22:30:10 +00:00
|
|
|
if err != nil {
|
2022-12-13 19:28:07 +00:00
|
|
|
return xerrors.Errorf("create circular buffer: %w", err)
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
|
2022-09-05 13:45:10 +00:00
|
|
|
ptty, process, err := pty.Start(cmd)
|
2022-04-29 22:30:10 +00:00
|
|
|
if err != nil {
|
2022-12-13 19:28:07 +00:00
|
|
|
return xerrors.Errorf("start command: %w", err)
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ctx, cancelFunc := context.WithCancel(ctx)
|
|
|
|
rpty = &reconnectingPTY{
|
2022-11-17 16:57:15 +00:00
|
|
|
activeConns: map[string]net.Conn{
|
|
|
|
// We have to put the connection in the map instantly otherwise
|
|
|
|
// the connection won't be closed if the process instantly dies.
|
|
|
|
connectionID: conn,
|
|
|
|
},
|
|
|
|
ptty: ptty,
|
2022-04-29 22:30:10 +00:00
|
|
|
// Timeouts created with an after func can be reset!
|
|
|
|
timeout: time.AfterFunc(a.reconnectingPTYTimeout, cancelFunc),
|
|
|
|
circularBuffer: circularBuffer,
|
|
|
|
}
|
2022-09-01 01:09:44 +00:00
|
|
|
a.reconnectingPTYs.Store(msg.ID, rpty)
|
2022-04-29 22:30:10 +00:00
|
|
|
go func() {
|
|
|
|
// CommandContext isn't respected for Windows PTYs right now,
|
|
|
|
// so we need to manually track the lifecycle.
|
|
|
|
// When the context has been completed either:
|
|
|
|
// 1. The timeout completed.
|
|
|
|
// 2. The parent context was canceled.
|
|
|
|
<-ctx.Done()
|
|
|
|
_ = process.Kill()
|
|
|
|
}()
|
|
|
|
go func() {
|
|
|
|
// If the process dies randomly, we should
|
|
|
|
// close the pty.
|
2022-07-27 19:23:28 +00:00
|
|
|
_ = process.Wait()
|
2022-04-29 22:30:10 +00:00
|
|
|
rpty.Close()
|
|
|
|
}()
|
2022-12-02 14:24:40 +00:00
|
|
|
if err = a.trackConnGoroutine(func() {
|
2022-04-29 22:30:10 +00:00
|
|
|
buffer := make([]byte, 1024)
|
|
|
|
for {
|
|
|
|
read, err := rpty.ptty.Output().Read(buffer)
|
|
|
|
if err != nil {
|
|
|
|
// When the PTY is closed, this is triggered.
|
|
|
|
break
|
|
|
|
}
|
|
|
|
part := buffer[:read]
|
2022-05-02 17:31:04 +00:00
|
|
|
rpty.circularBufferMutex.Lock()
|
2022-04-29 22:30:10 +00:00
|
|
|
_, err = rpty.circularBuffer.Write(part)
|
2022-05-02 17:31:04 +00:00
|
|
|
rpty.circularBufferMutex.Unlock()
|
2022-04-29 22:30:10 +00:00
|
|
|
if err != nil {
|
2022-12-13 19:28:07 +00:00
|
|
|
logger.Error(ctx, "write to circular buffer", slog.Error(err))
|
2022-04-29 22:30:10 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
rpty.activeConnsMutex.Lock()
|
|
|
|
for _, conn := range rpty.activeConns {
|
|
|
|
_, _ = conn.Write(part)
|
|
|
|
}
|
|
|
|
rpty.activeConnsMutex.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cleanup the process, PTY, and delete it's
|
|
|
|
// ID from memory.
|
|
|
|
_ = process.Kill()
|
|
|
|
rpty.Close()
|
2022-09-01 01:09:44 +00:00
|
|
|
a.reconnectingPTYs.Delete(msg.ID)
|
2022-12-02 14:24:40 +00:00
|
|
|
}); err != nil {
|
2022-12-13 19:28:07 +00:00
|
|
|
return xerrors.Errorf("start routine: %w", err)
|
2022-12-02 14:24:40 +00:00
|
|
|
}
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
// Resize the PTY to initial height + width.
|
2022-09-01 01:09:44 +00:00
|
|
|
err := rpty.ptty.Resize(msg.Height, msg.Width)
|
2022-04-29 22:30:10 +00:00
|
|
|
if err != nil {
|
|
|
|
// We can continue after this, it's not fatal!
|
2022-12-13 19:28:07 +00:00
|
|
|
logger.Error(ctx, "resize", slog.Error(err))
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
// Write any previously stored data for the TTY.
|
2022-05-02 17:31:04 +00:00
|
|
|
rpty.circularBufferMutex.RLock()
|
2022-12-13 19:28:07 +00:00
|
|
|
prevBuf := slices.Clone(rpty.circularBuffer.Bytes())
|
2022-05-02 17:31:04 +00:00
|
|
|
rpty.circularBufferMutex.RUnlock()
|
2022-12-13 19:28:07 +00:00
|
|
|
// Note that there is a small race here between writing buffered
|
|
|
|
// data and storing conn in activeConns. This is likely a very minor
|
|
|
|
// edge case, but we should look into ways to avoid it. Holding
|
|
|
|
// activeConnsMutex would be one option, but holding this mutex
|
|
|
|
// while also holding circularBufferMutex seems dangerous.
|
|
|
|
_, err = conn.Write(prevBuf)
|
2022-04-29 22:30:10 +00:00
|
|
|
if err != nil {
|
2022-12-13 19:28:07 +00:00
|
|
|
return xerrors.Errorf("write buffer to conn: %w", err)
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
// Multiple connections to the same TTY are permitted.
|
|
|
|
// This could easily be used for terminal sharing, but
|
|
|
|
// we do it because it's a nice user experience to
|
|
|
|
// copy/paste a terminal URL and have it _just work_.
|
|
|
|
rpty.activeConnsMutex.Lock()
|
|
|
|
rpty.activeConns[connectionID] = conn
|
|
|
|
rpty.activeConnsMutex.Unlock()
|
|
|
|
// Resetting this timeout prevents the PTY from exiting.
|
|
|
|
rpty.timeout.Reset(a.reconnectingPTYTimeout)
|
|
|
|
|
|
|
|
ctx, cancelFunc := context.WithCancel(ctx)
|
|
|
|
defer cancelFunc()
|
|
|
|
heartbeat := time.NewTicker(a.reconnectingPTYTimeout / 2)
|
|
|
|
defer heartbeat.Stop()
|
|
|
|
go func() {
|
|
|
|
// Keep updating the activity while this
|
|
|
|
// connection is alive!
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
case <-heartbeat.C:
|
|
|
|
}
|
|
|
|
rpty.timeout.Reset(a.reconnectingPTYTimeout)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
defer func() {
|
|
|
|
// After this connection ends, remove it from
|
|
|
|
// the PTYs active connections. If it isn't
|
|
|
|
// removed, all PTY data will be sent to it.
|
|
|
|
rpty.activeConnsMutex.Lock()
|
|
|
|
delete(rpty.activeConns, connectionID)
|
|
|
|
rpty.activeConnsMutex.Unlock()
|
|
|
|
}()
|
|
|
|
decoder := json.NewDecoder(conn)
|
2022-09-23 19:51:04 +00:00
|
|
|
var req codersdk.ReconnectingPTYRequest
|
2022-04-29 22:30:10 +00:00
|
|
|
for {
|
|
|
|
err = decoder.Decode(&req)
|
|
|
|
if xerrors.Is(err, io.EOF) {
|
2022-12-13 19:28:07 +00:00
|
|
|
return nil
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
2022-12-13 19:28:07 +00:00
|
|
|
logger.Warn(ctx, "read conn", slog.Error(err))
|
|
|
|
return nil
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
_, err = rpty.ptty.Input().Write([]byte(req.Data))
|
|
|
|
if err != nil {
|
2022-12-13 19:28:07 +00:00
|
|
|
logger.Warn(ctx, "write to pty", slog.Error(err))
|
|
|
|
return nil
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
// Check if a resize needs to happen!
|
|
|
|
if req.Height == 0 || req.Width == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
err = rpty.ptty.Resize(req.Height, req.Width)
|
|
|
|
if err != nil {
|
|
|
|
// We can continue after this, it's not fatal!
|
2022-12-13 19:28:07 +00:00
|
|
|
logger.Error(ctx, "resize", slog.Error(err))
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-19 05:13:32 +00:00
|
|
|
// isClosed returns whether the API is closed or not.
|
2022-03-30 22:59:54 +00:00
|
|
|
func (a *agent) isClosed() bool {
|
2022-02-19 05:13:32 +00:00
|
|
|
select {
|
2022-03-30 22:59:54 +00:00
|
|
|
case <-a.closed:
|
2022-02-19 05:13:32 +00:00
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-30 22:59:54 +00:00
|
|
|
func (a *agent) Close() error {
|
|
|
|
a.closeMutex.Lock()
|
|
|
|
defer a.closeMutex.Unlock()
|
|
|
|
if a.isClosed() {
|
2022-02-19 05:13:32 +00:00
|
|
|
return nil
|
|
|
|
}
|
2022-03-30 22:59:54 +00:00
|
|
|
close(a.closed)
|
|
|
|
a.closeCancel()
|
2022-09-01 01:09:44 +00:00
|
|
|
if a.network != nil {
|
|
|
|
_ = a.network.Close()
|
|
|
|
}
|
2022-03-30 22:59:54 +00:00
|
|
|
_ = a.sshServer.Close()
|
|
|
|
a.connCloseWait.Wait()
|
2022-02-19 05:13:32 +00:00
|
|
|
return nil
|
|
|
|
}
|
2022-04-29 22:30:10 +00:00
|
|
|
|
|
|
|
type reconnectingPTY struct {
|
|
|
|
activeConnsMutex sync.Mutex
|
|
|
|
activeConns map[string]net.Conn
|
|
|
|
|
2022-05-02 17:31:04 +00:00
|
|
|
circularBuffer *circbuf.Buffer
|
|
|
|
circularBufferMutex sync.RWMutex
|
|
|
|
timeout *time.Timer
|
|
|
|
ptty pty.PTY
|
2022-04-29 22:30:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Close ends all connections to the reconnecting
|
|
|
|
// PTY and clear the circular buffer.
|
|
|
|
func (r *reconnectingPTY) Close() {
|
|
|
|
r.activeConnsMutex.Lock()
|
|
|
|
defer r.activeConnsMutex.Unlock()
|
|
|
|
for _, conn := range r.activeConns {
|
|
|
|
_ = conn.Close()
|
|
|
|
}
|
|
|
|
_ = r.ptty.Close()
|
2022-08-23 16:07:31 +00:00
|
|
|
r.circularBufferMutex.Lock()
|
2022-04-29 22:30:10 +00:00
|
|
|
r.circularBuffer.Reset()
|
2022-08-23 16:07:31 +00:00
|
|
|
r.circularBufferMutex.Unlock()
|
2022-04-29 22:30:10 +00:00
|
|
|
r.timeout.Stop()
|
|
|
|
}
|
2022-05-18 14:10:40 +00:00
|
|
|
|
|
|
|
// Bicopy copies all of the data between the two connections and will close them
|
|
|
|
// after one or both of them are done writing. If the context is canceled, both
|
|
|
|
// of the connections will be closed.
|
|
|
|
func Bicopy(ctx context.Context, c1, c2 io.ReadWriteCloser) {
|
2022-10-17 16:45:29 +00:00
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
_ = c1.Close()
|
|
|
|
_ = c2.Close()
|
|
|
|
}()
|
2022-05-18 14:10:40 +00:00
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
copyFunc := func(dst io.WriteCloser, src io.Reader) {
|
2022-10-17 16:45:29 +00:00
|
|
|
defer func() {
|
|
|
|
wg.Done()
|
|
|
|
// If one side of the copy fails, ensure the other one exits as
|
|
|
|
// well.
|
|
|
|
cancel()
|
|
|
|
}()
|
2022-05-18 14:10:40 +00:00
|
|
|
_, _ = io.Copy(dst, src)
|
|
|
|
}
|
|
|
|
|
|
|
|
wg.Add(2)
|
|
|
|
go copyFunc(c1, c2)
|
|
|
|
go copyFunc(c2, c1)
|
|
|
|
|
|
|
|
// Convert waitgroup to a channel so we can also wait on the context.
|
|
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
|
|
defer close(done)
|
|
|
|
wg.Wait()
|
|
|
|
}()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
case <-done:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-24 12:22:20 +00:00
|
|
|
// isQuietLogin checks if the SSH server should perform a quiet login or not.
|
|
|
|
//
|
|
|
|
// https://github.com/openssh/openssh-portable/blob/25bd659cc72268f2858c5415740c442ee950049f/session.c#L816
|
|
|
|
func isQuietLogin(rawCommand string) bool {
|
|
|
|
// We are always quiet unless this is a login shell.
|
|
|
|
if len(rawCommand) != 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Best effort, if we can't get the home directory,
|
|
|
|
// we can't lookup .hushlogin.
|
|
|
|
homedir, err := userHomeDir()
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = os.Stat(filepath.Join(homedir, ".hushlogin"))
|
|
|
|
return err == nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// showMOTD will output the message of the day from
|
|
|
|
// the given filename to dest, if the file exists.
|
|
|
|
//
|
|
|
|
// https://github.com/openssh/openssh-portable/blob/25bd659cc72268f2858c5415740c442ee950049f/session.c#L784
|
|
|
|
func showMOTD(dest io.Writer, filename string) error {
|
|
|
|
if filename == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
f, err := os.Open(filename)
|
2022-05-18 14:10:40 +00:00
|
|
|
if err != nil {
|
2022-11-24 12:22:20 +00:00
|
|
|
if xerrors.Is(err, os.ErrNotExist) {
|
|
|
|
// This is not an error, there simply isn't a MOTD to show.
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return xerrors.Errorf("open MOTD: %w", err)
|
2022-05-18 14:10:40 +00:00
|
|
|
}
|
2022-11-24 12:22:20 +00:00
|
|
|
defer f.Close()
|
2022-05-18 14:10:40 +00:00
|
|
|
|
2022-11-24 12:22:20 +00:00
|
|
|
s := bufio.NewScanner(f)
|
|
|
|
for s.Scan() {
|
|
|
|
// Carriage return ensures each line starts
|
|
|
|
// at the beginning of the terminal.
|
|
|
|
_, err = fmt.Fprint(dest, s.Text()+"\r\n")
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("write MOTD: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := s.Err(); err != nil {
|
|
|
|
return xerrors.Errorf("read MOTD: %w", err)
|
2022-05-18 14:10:40 +00:00
|
|
|
}
|
|
|
|
|
2022-11-24 12:22:20 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// userHomeDir returns the home directory of the current user, giving
|
|
|
|
// priority to the $HOME environment variable.
|
|
|
|
func userHomeDir() (string, error) {
|
|
|
|
// First we check the environment.
|
|
|
|
homedir, err := os.UserHomeDir()
|
|
|
|
if err == nil {
|
|
|
|
return homedir, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// As a fallback, we try the user information.
|
|
|
|
u, err := user.Current()
|
|
|
|
if err != nil {
|
|
|
|
return "", xerrors.Errorf("current user: %w", err)
|
|
|
|
}
|
|
|
|
return u.HomeDir, nil
|
2022-05-18 14:10:40 +00:00
|
|
|
}
|