Skip to content

Commit 2f297ae

Browse files
committed
Merge branch 'main' into taildefault
2 parents c2bc664 + 0e59cb2 commit 2f297ae

File tree

26 files changed

+896
-244
lines changed

26 files changed

+896
-244
lines changed

.github/workflows/dependabot.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Dependabot is annoying, but this makes it a bit less so.
2+
name: Auto Approve Dependabot
3+
4+
on: pull_request_target
5+
6+
jobs:
7+
auto-approve:
8+
runs-on: ubuntu-latest
9+
permissions:
10+
pull-requests: write
11+
steps:
12+
- uses: hmarr/auto-approve-action@v2
13+
if: github.actor == 'dependabot[bot]'

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"gonet",
3232
"gossh",
3333
"gsyslog",
34+
"GTTY",
3435
"hashicorp",
3536
"hclsyntax",
3637
"httpapi",
@@ -67,6 +68,7 @@
6768
"ntqry",
6869
"OIDC",
6970
"oneof",
71+
"opty",
7072
"paralleltest",
7173
"parameterscopeid",
7274
"pqtype",
@@ -76,6 +78,7 @@
7678
"provisionerd",
7779
"provisionersdk",
7880
"ptty",
81+
"ptys",
7982
"ptytest",
8083
"reconfig",
8184
"retrier",
@@ -87,6 +90,7 @@
8790
"sourcemapped",
8891
"Srcs",
8992
"stretchr",
93+
"STTY",
9094
"stuntest",
9195
"tailbroker",
9296
"tailcfg",
@@ -105,6 +109,7 @@
105109
"tfjson",
106110
"tfplan",
107111
"tfstate",
112+
"tios",
108113
"tparallel",
109114
"trimprefix",
110115
"tsdial",

agent/agent.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ func (a *agent) runStartupScript(ctx context.Context, script string) error {
374374
return nil
375375
}
376376

377-
writer, err := os.OpenFile(filepath.Join(os.TempDir(), "coder-startup-script.log"), os.O_CREATE|os.O_RDWR, 0600)
377+
writer, err := os.OpenFile(filepath.Join(os.TempDir(), "coder-startup-script.log"), os.O_CREATE|os.O_RDWR, 0o600)
378378
if err != nil {
379379
return xerrors.Errorf("open startup script log file: %w", err)
380380
}
@@ -537,6 +537,8 @@ func (a *agent) init(ctx context.Context) {
537537
},
538538
SubsystemHandlers: map[string]ssh.SubsystemHandler{
539539
"sftp": func(session ssh.Session) {
540+
session.DisablePTYEmulation()
541+
540542
server, err := sftp.NewServer(session)
541543
if err != nil {
542544
a.logger.Debug(session.Context(), "initialize sftp server", slog.Error(err))
@@ -661,7 +663,8 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
661663
}
662664

663665
func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
664-
cmd, err := a.createCommand(session.Context(), session.RawCommand(), session.Environ())
666+
ctx := session.Context()
667+
cmd, err := a.createCommand(ctx, session.RawCommand(), session.Environ())
665668
if err != nil {
666669
return err
667670
}
@@ -678,32 +681,34 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
678681

679682
sshPty, windowSize, isPty := session.Pty()
680683
if isPty {
684+
// Disable minimal PTY emulation set by gliderlabs/ssh (NL-to-CRNL).
685+
// See https://github.com/coder/coder/issues/3371.
686+
session.DisablePTYEmulation()
687+
681688
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", sshPty.Term))
682689

683690
// The pty package sets `SSH_TTY` on supported platforms.
684-
ptty, process, err := pty.Start(cmd)
691+
ptty, process, err := pty.Start(cmd, pty.WithPTYOption(
692+
pty.WithSSHRequest(sshPty),
693+
pty.WithLogger(slog.Stdlib(ctx, a.logger, slog.LevelInfo)),
694+
))
685695
if err != nil {
686696
return xerrors.Errorf("start command: %w", err)
687697
}
688698
defer func() {
689699
closeErr := ptty.Close()
690700
if closeErr != nil {
691-
a.logger.Warn(context.Background(), "failed to close tty",
692-
slog.Error(closeErr))
701+
a.logger.Warn(ctx, "failed to close tty", slog.Error(closeErr))
693702
if retErr == nil {
694703
retErr = closeErr
695704
}
696705
}
697706
}()
698-
err = ptty.Resize(uint16(sshPty.Window.Height), uint16(sshPty.Window.Width))
699-
if err != nil {
700-
return xerrors.Errorf("resize ptty: %w", err)
701-
}
702707
go func() {
703708
for win := range windowSize {
704709
resizeErr := ptty.Resize(uint16(win.Height), uint16(win.Width))
705710
if resizeErr != nil {
706-
a.logger.Warn(context.Background(), "failed to resize tty", slog.Error(resizeErr))
711+
a.logger.Warn(ctx, "failed to resize tty", slog.Error(resizeErr))
707712
}
708713
}
709714
}()
@@ -718,8 +723,7 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
718723
// ExitErrors just mean the command we run returned a non-zero exit code, which is normal
719724
// and not something to be concerned about. But, if it's something else, we should log it.
720725
if err != nil && !xerrors.As(err, &exitErr) {
721-
a.logger.Warn(context.Background(), "wait error",
722-
slog.Error(err))
726+
a.logger.Warn(ctx, "wait error", slog.Error(err))
723727
}
724728
return err
725729
}

cli/gitssh.go

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package cli
22

33
import (
4+
"bufio"
5+
"bytes"
6+
"context"
47
"fmt"
8+
"io"
59
"os"
610
"os/exec"
11+
"os/signal"
12+
"path/filepath"
713
"strings"
814

915
"github.com/spf13/cobra"
@@ -13,16 +19,30 @@ import (
1319
)
1420

1521
func gitssh() *cobra.Command {
16-
return &cobra.Command{
22+
cmd := &cobra.Command{
1723
Use: "gitssh",
1824
Hidden: true,
1925
Short: `Wraps the "ssh" command and uses the coder gitssh key for authentication`,
2026
RunE: func(cmd *cobra.Command, args []string) error {
27+
ctx := cmd.Context()
28+
env := os.Environ()
29+
30+
// Catch interrupt signals to ensure the temporary private
31+
// key file is cleaned up on most cases.
32+
ctx, stop := signal.NotifyContext(ctx, interruptSignals...)
33+
defer stop()
34+
35+
// Early check so errors are reported immediately.
36+
identityFiles, err := parseIdentityFilesForHost(ctx, args, env)
37+
if err != nil {
38+
return err
39+
}
40+
2141
client, err := createAgentClient(cmd)
2242
if err != nil {
2343
return xerrors.Errorf("create agent client: %w", err)
2444
}
25-
key, err := client.AgentGitSSHKey(cmd.Context())
45+
key, err := client.AgentGitSSHKey(ctx)
2646
if err != nil {
2747
return xerrors.Errorf("get agent git ssh token: %w", err)
2848
}
@@ -44,8 +64,23 @@ func gitssh() *cobra.Command {
4464
return xerrors.Errorf("close temp gitsshkey file: %w", err)
4565
}
4666

47-
args = append([]string{"-i", privateKeyFile.Name()}, args...)
48-
c := exec.CommandContext(cmd.Context(), "ssh", args...)
67+
// Append our key, giving precedence to user keys. Note that
68+
// OpenSSH server are typically configured with MaxAuthTries
69+
// set to the default value of 6. This means that only the 6
70+
// first keys can be tried. However, we will assume that if
71+
// a user has configured 6+ keys for a host, they know what
72+
// they're doing. This behavior is critical if a server has
73+
// been configured with MaxAuthTries set to 1.
74+
identityFiles = append(identityFiles, privateKeyFile.Name())
75+
76+
var identityArgs []string
77+
for _, id := range identityFiles {
78+
identityArgs = append(identityArgs, "-i", id)
79+
}
80+
81+
args = append(identityArgs, args...)
82+
c := exec.CommandContext(ctx, "ssh", args...)
83+
c.Env = append(c.Env, env...)
4984
c.Stderr = cmd.ErrOrStderr()
5085
c.Stdout = cmd.OutOrStdout()
5186
c.Stdin = cmd.InOrStdin()
@@ -69,4 +104,86 @@ func gitssh() *cobra.Command {
69104
return nil
70105
},
71106
}
107+
108+
return cmd
109+
}
110+
111+
// fallbackIdentityFiles is the list of identity files SSH tries when
112+
// none have been defined for a host.
113+
var fallbackIdentityFiles = strings.Join([]string{
114+
"identityfile ~/.ssh/id_rsa",
115+
"identityfile ~/.ssh/id_dsa",
116+
"identityfile ~/.ssh/id_ecdsa",
117+
"identityfile ~/.ssh/id_ecdsa_sk",
118+
"identityfile ~/.ssh/id_ed25519",
119+
"identityfile ~/.ssh/id_ed25519_sk",
120+
"identityfile ~/.ssh/id_xmss",
121+
}, "\n")
122+
123+
// parseIdentityFilesForHost uses ssh -G to discern what SSH keys have
124+
// been enabled for the host (via the users SSH config) and returns a
125+
// list of existing identity files.
126+
//
127+
// We do this because when no keys are defined for a host, SSH uses
128+
// fallback keys (see above). However, by passing `-i` to attach our
129+
// private key, we're effectively disabling the fallback keys.
130+
//
131+
// Example invocation:
132+
//
133+
// ssh -G -o SendEnv=GIT_PROTOCOL git@github.com git-upload-pack 'coder/coder'
134+
//
135+
// The extra arguments work without issue and lets us run the command
136+
// as-is without stripping out the excess (git-upload-pack 'coder/coder').
137+
func parseIdentityFilesForHost(ctx context.Context, args, env []string) (identityFiles []string, error error) {
138+
home, err := os.UserHomeDir()
139+
if err != nil {
140+
return nil, xerrors.Errorf("get user home dir failed: %w", err)
141+
}
142+
143+
var outBuf bytes.Buffer
144+
var r io.Reader = &outBuf
145+
146+
args = append([]string{"-G"}, args...)
147+
cmd := exec.CommandContext(ctx, "ssh", args...)
148+
cmd.Env = append(cmd.Env, env...)
149+
cmd.Stdout = &outBuf
150+
cmd.Stderr = io.Discard
151+
err = cmd.Run()
152+
if err != nil {
153+
// If ssh -G failed, the SSH version is likely too old, fallback
154+
// to using the default identity files.
155+
r = strings.NewReader(fallbackIdentityFiles)
156+
}
157+
158+
s := bufio.NewScanner(r)
159+
for s.Scan() {
160+
line := s.Text()
161+
if strings.HasPrefix(line, "identityfile ") {
162+
id := strings.TrimPrefix(line, "identityfile ")
163+
if strings.HasPrefix(id, "~/") {
164+
id = home + id[1:]
165+
}
166+
// OpenSSH on Windows is weird, it supports using (and does
167+
// use) mixed \ and / in paths.
168+
//
169+
// Example: C:\Users\ZeroCool/.ssh/known_hosts
170+
//
171+
// To check the file existence in Go, though, we want to use
172+
// proper Windows paths.
173+
// OpenSSH is amazing, this will work on Windows too:
174+
// C:\Users\ZeroCool/.ssh/id_rsa
175+
id = filepath.FromSlash(id)
176+
177+
// Only include the identity file if it exists.
178+
if _, err := os.Stat(id); err == nil {
179+
identityFiles = append(identityFiles, id)
180+
}
181+
}
182+
}
183+
if err := s.Err(); err != nil {
184+
// This should never happen, the check is for completeness.
185+
return nil, xerrors.Errorf("scan ssh output: %w", err)
186+
}
187+
188+
return identityFiles, nil
72189
}

0 commit comments

Comments
 (0)