• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fornellas / resonance / 15767238418

19 Jun 2025 10:24PM UTC coverage: 53.944% (-1.5%) from 55.486%
15767238418

Pull #301

github

web-flow
Merge 00654e985 into babe9bde0
Pull Request #301: APT Pakcage: improve functionality

55 of 343 new or added lines in 7 files covered. (16.03%)

7 existing lines in 1 file now uncovered.

3768 of 6985 relevant lines covered (53.94%)

14.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.96
/host/sudo_wrapper.go
1
package host
2

3
import (
4
        "bytes"
5
        "context"
6
        "crypto/rand"
7
        "crypto/sha256"
8
        "encoding/hex"
9
        "errors"
10
        "fmt"
11
        "io"
12
        "os"
13
        "strings"
14
        "sync"
15

16
        "al.essio.dev/pkg/shellescape"
17
        "golang.org/x/term"
18

19
        "github.com/fornellas/slogxt/log"
20

21
        "github.com/fornellas/resonance/host/types"
22
)
23

24
// stdinSudo prevents stdin from being read, before we can detect output
25
// from sudo on stdout. This is required because os/exec and ssh buffer stdin
26
// before there's any read, meaning we can't intercept the sudo prompt
27
// reliably
28
type stdinSudo struct {
29
        Unlock   chan struct{}
30
        SendPass chan string
31
        Reader   io.Reader
32
        mutex    sync.Mutex
33
        unlocked bool
34
}
35

36
func (r *stdinSudo) Read(p []byte) (int, error) {
11✔
37
        r.mutex.Lock()
11✔
38
        defer r.mutex.Unlock()
11✔
39

11✔
40
        if !r.unlocked {
21✔
41
                select {
10✔
42
                case <-r.Unlock:
10✔
43
                        r.unlocked = true
10✔
44
                case password := <-r.SendPass:
×
45
                        var passwordBytes []byte
×
46
                        passwordBytes = fmt.Appendf(passwordBytes, "%s\n", password)
×
47
                        if len(passwordBytes) > len(p) {
×
48
                                return 0, fmt.Errorf(
×
49
                                        "password is longer (%d) than read buffer (%d)", len(passwordBytes), len(p),
×
50
                                )
×
51
                        }
×
52
                        copy(p, passwordBytes)
×
53
                        return len(passwordBytes), nil
×
54
                }
55
        }
56

57
        return r.Reader.Read(p)
11✔
58
}
59

60
// stderrSudo waits for either write:
61
// - sudo prompt: asks for password, caches it, and send to stdin.
62
// - sudo ok: unlocks stdin.
63
type stderrSudo struct {
64
        Unlock          chan struct{}
65
        SendPass        chan string
66
        Prompt          []byte
67
        SudoOk          []byte
68
        Writer          io.Writer
69
        Password        **string
70
        mutex           sync.Mutex
71
        unlocked        bool
72
        passwordAttempt *string
73
}
74

75
func (w *stderrSudo) Write(p []byte) (_ int, retErr error) {
12✔
76
        w.mutex.Lock()
12✔
77
        defer w.mutex.Unlock()
12✔
78

12✔
79
        var extraLen int
12✔
80

12✔
81
        if !w.unlocked {
22✔
82
                if bytes.Contains(p, w.Prompt) {
10✔
83
                        var password string
×
84
                        if *w.Password == nil {
×
85
                                state, err := term.MakeRaw(int(os.Stdin.Fd()))
×
86
                                if err != nil {
×
87
                                        return 0, err
×
88
                                }
×
89
                                defer func() {
×
90
                                        retErr = errors.Join(retErr, term.Restore(int(os.Stdin.Fd()), state))
×
91
                                }()
×
92

93
                                var passwordBytes []byte
×
94
                                fmt.Printf("sudo password: ")
×
NEW
95
                                passwordBytes, err = term.ReadPassword(int(os.Stdin.Fd()))
×
96
                                if err != nil {
×
97
                                        return 0, err
×
98
                                }
×
99
                                fmt.Printf("\n\r")
×
100
                                password = string(passwordBytes)
×
101
                                w.passwordAttempt = &password
×
102
                        } else {
×
103
                                password = **w.Password
×
104
                        }
×
105
                        w.SendPass <- password
×
106
                        extraLen = len(w.Prompt)
×
107
                        p = bytes.ReplaceAll(p, w.Prompt, []byte{})
×
108
                } else if bytes.Contains(p, w.SudoOk) {
20✔
109
                        if w.passwordAttempt != nil {
10✔
110
                                *w.Password = w.passwordAttempt
×
111
                        }
×
112
                        w.Unlock <- struct{}{}
10✔
113
                        w.unlocked = true
10✔
114
                        extraLen = len(w.SudoOk)
10✔
115
                        p = bytes.ReplaceAll(p, w.SudoOk, []byte{})
10✔
116
                }
117
        }
118

119
        n, err := w.Writer.Write(p)
12✔
120
        return n + extraLen, err
12✔
121
}
122

123
// SudoWrapper wraps another BaseHost and runs all commands with sudo.
124
type SudoWrapper struct {
125
        BaseHost types.BaseHost
126
        Password *string
127
        envPath  string
128
}
129

130
func NewSudoWrapper(ctx context.Context, baseHost types.BaseHost) (*SudoWrapper, error) {
3✔
131
        ctx, _ = log.MustWithGroup(ctx, "⚡ Sudo")
3✔
132

3✔
133
        sudoWrapper := SudoWrapper{
3✔
134
                BaseHost: baseHost,
3✔
135
        }
3✔
136

3✔
137
        cmd := types.Cmd{
3✔
138
                Path: "true",
3✔
139
        }
3✔
140
        waitStatus, err := sudoWrapper.Run(ctx, cmd)
3✔
141
        if err != nil {
3✔
142
                return nil, err
×
143
        }
×
144
        if !waitStatus.Success() {
3✔
145
                return nil, fmt.Errorf("failed to run %s: %s", cmd, waitStatus.String())
×
146
        }
×
147

148
        if err := sudoWrapper.setEnvPath(ctx); err != nil {
3✔
149
                return nil, err
×
150
        }
×
151

152
        return &sudoWrapper, nil
3✔
153
}
154

155
func (h *SudoWrapper) getRandomString() string {
20✔
156
        bytes := make([]byte, 32)
20✔
157
        _, err := rand.Read(bytes)
20✔
158
        if err != nil {
20✔
159
                panic(err)
×
160
        }
161
        hash := sha256.Sum256(bytes)
20✔
162
        return hex.EncodeToString(hash[:])
20✔
163
}
164

165
func (h *SudoWrapper) runEnv(ctx context.Context, cmd types.Cmd, ignoreCmdEnv bool) (types.WaitStatus, error) {
11✔
166
        prompt := fmt.Sprintf("sudo password (%s)", h.getRandomString())
11✔
167
        sudoOk := fmt.Sprintf("sudo ok (%s)", h.getRandomString())
11✔
168

11✔
169
        shellCmdArgs := []string{shellescape.Quote(cmd.Path)}
11✔
170
        for _, arg := range cmd.Args {
18✔
171
                shellCmdArgs = append(shellCmdArgs, shellescape.Quote(arg))
7✔
172
        }
7✔
173
        shellCmdStr := strings.Join(shellCmdArgs, " ")
11✔
174

11✔
175
        if cmd.Dir == "" {
20✔
176
                cmd.Dir = "/tmp"
9✔
177
        }
9✔
178

179
        cmd.Path = "sudo"
11✔
180

11✔
181
        if !ignoreCmdEnv {
21✔
182
                if len(cmd.Env) == 0 {
19✔
183
                        cmd.Env = []string{"LANG=en_US.UTF-8"}
9✔
184
                        if h.envPath != "" {
17✔
185
                                cmd.Env = append(cmd.Env, h.envPath)
8✔
186
                        }
8✔
187
                }
188
                envStrs := []string{}
10✔
189
                for _, nameValue := range cmd.Env {
26✔
190
                        envStrs = append(envStrs, shellescape.Quote(nameValue))
16✔
191
                }
16✔
192
                cmd.Args = []string{
10✔
193
                        "--stdin",
10✔
194
                        "--prompt", prompt,
10✔
195
                        "--", "sh", "-c",
10✔
196
                        fmt.Sprintf(
10✔
197
                                "echo -n %s 1>&2 && cd %s && exec env --ignore-environment %s %s",
10✔
198
                                shellescape.Quote(sudoOk), cmd.Dir, strings.Join(envStrs, " "), shellCmdStr,
10✔
199
                        ),
10✔
200
                }
10✔
201
        } else {
3✔
202
                cmd.Args = []string{
3✔
203
                        "--stdin",
3✔
204
                        "--prompt", prompt,
3✔
205
                        "--", "sh", "-c",
3✔
206
                        fmt.Sprintf(
3✔
207
                                "echo -n %s 1>&2 && cd %s && exec %s",
3✔
208
                                shellescape.Quote(sudoOk), cmd.Dir, shellCmdStr,
3✔
209
                        ),
3✔
210
                }
3✔
211
        }
3✔
212

213
        unlockStdin := make(chan struct{}, 1)
11✔
214
        sendPassStdin := make(chan string, 1)
11✔
215

11✔
216
        var stdin io.Reader
11✔
217
        if cmd.Stdin != nil {
14✔
218
                stdin = cmd.Stdin
3✔
219
        } else {
13✔
220
                stdin = &bytes.Buffer{}
10✔
221
        }
10✔
222
        cmd.Stdin = &stdinSudo{
11✔
223
                Unlock:   unlockStdin,
11✔
224
                SendPass: sendPassStdin,
11✔
225
                Reader:   stdin,
11✔
226
        }
11✔
227

11✔
228
        var stderr io.Writer
11✔
229
        if cmd.Stderr != nil {
21✔
230
                stderr = cmd.Stderr
10✔
231
        } else {
13✔
232
                stderr = io.Discard
3✔
233
        }
3✔
234
        cmd.Stderr = &stderrSudo{
11✔
235
                Unlock:   unlockStdin,
11✔
236
                SendPass: sendPassStdin,
11✔
237
                Prompt:   []byte(prompt),
11✔
238
                SudoOk:   []byte(sudoOk),
11✔
239
                Writer:   stderr,
11✔
240
                Password: &h.Password,
11✔
241
        }
11✔
242

11✔
243
        // we always run the command over env, so, when a command does not exist, it will return 127,
11✔
244
        // and that's the quicky way we can detect os.ErrNotExist here.
11✔
245
        waitStatus, err := h.BaseHost.Run(ctx, cmd)
11✔
246
        if err == nil && waitStatus.Exited && waitStatus.ExitCode == 127 {
14✔
247
                return types.WaitStatus{}, os.ErrNotExist
3✔
248
        }
3✔
249
        return waitStatus, err
10✔
250
}
251

252
func (h *SudoWrapper) Run(ctx context.Context, cmd types.Cmd) (types.WaitStatus, error) {
10✔
253
        return h.runEnv(ctx, cmd, false)
10✔
254
}
10✔
255

256
func (h SudoWrapper) String() string {
3✔
257
        return h.BaseHost.String()
3✔
258
}
3✔
259

260
func (h SudoWrapper) Type() string {
3✔
261
        return h.BaseHost.Type()
3✔
262
}
3✔
263

264
func (h SudoWrapper) Close(ctx context.Context) error {
3✔
265
        return h.BaseHost.Close(ctx)
3✔
266
}
3✔
267

268
func (h *SudoWrapper) setEnvPath(ctx context.Context) error {
3✔
269
        stdoutBuffer := bytes.Buffer{}
3✔
270
        stderrBuffer := bytes.Buffer{}
3✔
271
        cmd := types.Cmd{
3✔
272
                Path:   "env",
3✔
273
                Stdout: &stdoutBuffer,
3✔
274
                Stderr: &stderrBuffer,
3✔
275
        }
3✔
276
        waitStatus, err := h.runEnv(ctx, cmd, true)
3✔
277
        if err != nil {
3✔
278
                return err
×
279
        }
×
280
        if !waitStatus.Success() {
3✔
281
                return fmt.Errorf(
×
282
                        "failed to run %s: %s\nstdout:\n%s\nstderr:\n%s",
×
283
                        cmd, waitStatus.String(), stdoutBuffer.String(), stderrBuffer.String(),
×
284
                )
×
285
        }
×
286
        strings.SplitSeq(stdoutBuffer.String(), "\n")(func(value string) bool {
7✔
287
                if strings.HasPrefix(value, "PATH=") {
7✔
288
                        h.envPath = value
3✔
289
                        return false
3✔
290
                }
3✔
291
                return true
3✔
292
        })
293
        return nil
3✔
294
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc