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

fornellas / resonance / 14681039432

26 Apr 2025 12:05PM UTC coverage: 57.102% (+0.3%) from 56.807%
14681039432

Pull #262

github

web-flow
Merge d5a56d147 into 194d32297
Pull Request #262: Refactor Logger

662 of 831 new or added lines in 29 files covered. (79.66%)

8 existing lines in 5 files now uncovered.

3791 of 6639 relevant lines covered (57.1%)

22.76 hits per line

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

75.62
/host/sudo_wrapper.go
1
package host
2

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

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

18
        "github.com/fornellas/resonance/host/types"
19
        "github.com/fornellas/resonance/log"
20
)
21

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

34
func (r *StdinSudo) Read(p []byte) (int, error) {
10✔
35
        r.mutex.Lock()
10✔
36
        defer r.mutex.Unlock()
10✔
37

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

55
        return r.Reader.Read(p)
10✔
56
}
57

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

73
func (w *StderrSudo) Write(p []byte) (int, error) {
10✔
74
        w.mutex.Lock()
10✔
75
        defer w.mutex.Unlock()
10✔
76

10✔
77
        var extraLen int
10✔
78

10✔
79
        if !w.unlocked {
19✔
80
                if bytes.Contains(p, w.Prompt) {
9✔
81
                        var password string
×
82
                        if *w.Password == nil {
×
83
                                state, err := term.MakeRaw(int(os.Stdin.Fd()))
×
84
                                if err != nil {
×
85
                                        return 0, err
×
86
                                }
×
87
                                defer term.Restore(int(os.Stdin.Fd()), state)
×
88

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

115
        len, err := w.Writer.Write(p)
10✔
116
        return len + extraLen, err
10✔
117
}
118

119
// SudoWrapper wraps another BaseHost and runs all commands with sudo.
120
type SudoWrapper struct {
121
        BaseHost types.BaseHost
122
        Password *string
123
        envPath  string
124
}
125

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

3✔
129
        sudoWrapper := SudoWrapper{
3✔
130
                BaseHost: baseHost,
3✔
131
        }
3✔
132

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

144
        if err := sudoWrapper.setEnvPath(ctx); err != nil {
3✔
145
                return nil, err
×
146
        }
×
147

148
        return &sudoWrapper, nil
3✔
149
}
150

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

161
func (h *SudoWrapper) runEnv(ctx context.Context, cmd types.Cmd, ignoreCmdEnv bool) (types.WaitStatus, error) {
10✔
162
        prompt := fmt.Sprintf("sudo password (%s)", h.getRandomString())
10✔
163
        sudoOk := fmt.Sprintf("sudo ok (%s)", h.getRandomString())
10✔
164

10✔
165
        shellCmdArgs := []string{shellescape.Quote(cmd.Path)}
10✔
166
        for _, arg := range cmd.Args {
17✔
167
                shellCmdArgs = append(shellCmdArgs, shellescape.Quote(arg))
7✔
168
        }
7✔
169
        shellCmdStr := strings.Join(shellCmdArgs, " ")
10✔
170

10✔
171
        if cmd.Dir == "" {
18✔
172
                cmd.Dir = "/tmp"
8✔
173
        }
8✔
174

175
        cmd.Path = "sudo"
10✔
176

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

209
        unlockStdin := make(chan struct{}, 1)
10✔
210
        sendPassStdin := make(chan string, 1)
10✔
211

10✔
212
        var stdin io.Reader
10✔
213
        if cmd.Stdin != nil {
13✔
214
                stdin = cmd.Stdin
3✔
215
        } else {
12✔
216
                stdin = &bytes.Buffer{}
9✔
217
        }
9✔
218
        cmd.Stdin = &StdinSudo{
10✔
219
                Unlock:   unlockStdin,
10✔
220
                SendPass: sendPassStdin,
10✔
221
                Reader:   stdin,
10✔
222
        }
10✔
223

10✔
224
        var stderr io.Writer
10✔
225
        if cmd.Stderr != nil {
19✔
226
                stderr = cmd.Stderr
9✔
227
        } else {
12✔
228
                stderr = io.Discard
3✔
229
        }
3✔
230
        cmd.Stderr = &StderrSudo{
10✔
231
                Unlock:   unlockStdin,
10✔
232
                SendPass: sendPassStdin,
10✔
233
                Prompt:   []byte(prompt),
10✔
234
                SudoOk:   []byte(sudoOk),
10✔
235
                Writer:   stderr,
10✔
236
                Password: &h.Password,
10✔
237
        }
10✔
238

10✔
239
        return h.BaseHost.Run(ctx, cmd)
10✔
240
}
241

242
func (h *SudoWrapper) Run(ctx context.Context, cmd types.Cmd) (types.WaitStatus, error) {
9✔
243
        return h.runEnv(ctx, cmd, false)
9✔
244
}
9✔
245

246
func (h SudoWrapper) String() string {
3✔
247
        return h.BaseHost.String()
3✔
248
}
3✔
249

250
func (h SudoWrapper) Type() string {
3✔
251
        return h.BaseHost.Type()
3✔
252
}
3✔
253

254
func (h SudoWrapper) Close(ctx context.Context) error {
3✔
255
        return h.BaseHost.Close(ctx)
3✔
256
}
3✔
257

258
func (h *SudoWrapper) setEnvPath(ctx context.Context) error {
3✔
259
        stdoutBuffer := bytes.Buffer{}
3✔
260
        stderrBuffer := bytes.Buffer{}
3✔
261
        cmd := types.Cmd{
3✔
262
                Path:   "env",
3✔
263
                Stdout: &stdoutBuffer,
3✔
264
                Stderr: &stderrBuffer,
3✔
265
        }
3✔
266
        waitStatus, err := h.runEnv(ctx, cmd, true)
3✔
267
        if err != nil {
3✔
268
                return err
×
269
        }
×
270
        if !waitStatus.Success() {
3✔
271
                return fmt.Errorf(
×
272
                        "failed to run %s: %s\nstdout:\n%s\nstderr:\n%s",
×
273
                        cmd, waitStatus.String(), stdoutBuffer.String(), stderrBuffer.String(),
×
274
                )
×
275
        }
×
276
        strings.SplitSeq(stdoutBuffer.String(), "\n")(func(value string) bool {
7✔
277
                if strings.HasPrefix(value, "PATH=") {
7✔
278
                        h.envPath = value
3✔
279
                        return false
3✔
280
                }
3✔
281
                return true
3✔
282
        })
283
        return nil
3✔
284
}
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