Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions cmd/hermes-node/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,6 @@ func (fs *fakeServer) URL() string {

func (fs *fakeServer) Close() { fs.srv.Close() }

func (fs *fakeServer) setNextResp(env map[string]any) {
fs.mu.Lock()
defer fs.mu.Unlock()
fs.nextResp = env
}

func (fs *fakeServer) handle(w http.ResponseWriter, r *http.Request) {
conn, err := fs.upgrader.Upgrade(w, r, nil)
if err != nil {
Expand Down
34 changes: 26 additions & 8 deletions internal/exec/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
// merges stderr into stdout; the adapter preserves that contract.
package exec

import "context"
import (
"context"
"strings"
)

// SessionAdapter is the bridge *Session → wire.Executer. The
// `target` argument is accepted to match the forward-compatible
// Executer signature; on 1.4a the shell does its own cwd
// handling so target is dropped on the floor.
// SessionAdapter is the bridge *Session → wire.Executer.
//
// The `target` argument is the validated, symlink-resolved working
// directory the caller intended the command to run in. When
// non-empty, the adapter prepends an explicit "cd <target>" to
// the command so it executes in that directory regardless of the
// bash session's current state. When empty, the command runs in
// whatever cwd the shell's previous command left behind (backward-
// compatible behaviour).
type SessionAdapter struct {
S *Session
}
Expand All @@ -19,9 +27,19 @@ func NewSessionAdapter(s *Session) *SessionAdapter {
return &SessionAdapter{S: s}
}

// Run forwards to the underlying session. See SessionAdapter
// for the target-discard rationale.
func (a *SessionAdapter) Run(ctx context.Context, _, cmd string) (string, string, int, error) {
// shellQuote wraps s in single quotes, escaping any embedded
// single quotes per POSIX convention.
func shellQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
}

// Run forwards to the underlying session, prepending an explicit
// `cd <target>` when target is non-empty so the command executes
// in the directory the caller asked for.
func (a *SessionAdapter) Run(ctx context.Context, target, cmd string) (string, string, int, error) {
if target != "" {
cmd = "cd " + shellQuote(target) + "\n" + cmd
}
return a.S.Run(ctx, cmd)
}

Expand Down
1 change: 0 additions & 1 deletion internal/exec/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ type pendingCall struct {
// the END marker (or EOF) for a call.
type runResult struct {
stdout string
stderr string
exitSet bool
exit int
cwdSet bool
Expand Down
9 changes: 1 addition & 8 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,7 @@ const (
LevelError
)

var levelNames = map[Level]string{
LevelDebug: "DEBUG",
LevelInfo: "INFO",
LevelWarn: "WARN",
LevelError: "ERROR",
}

// ParseLevel converts a string to a Level. Returns LevelInfo and
// ParseLevel maps a case-insensitive string to a Level.
// false when the string is not recognised.
func ParseLevel(s string) (Level, bool) {
switch strings.ToUpper(strings.TrimSpace(s)) {
Expand Down
2 changes: 1 addition & 1 deletion internal/wire/handler_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ func (h *ExecHandler) Handle(ctx context.Context, requestID string, payload map[
// ignored. The protocol field is preserved in the decoded
// payload so the upgrade is mechanical.
start := h.now()
stdout, stderr, exit, runErr := h.Shell.Run(callCtx, cwd, p.Command)
stdout, stderr, exit, runErr := h.Shell.Run(callCtx, canonical, p.Command)
duration := h.now().Sub(start)

// Prepend auto-cd warning to stderr if the cwd was outside
Expand Down
10 changes: 7 additions & 3 deletions internal/wire/handler_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,11 @@ func (fsys *FileSystem) ReadHandler(ctx context.Context, requestID string, paylo
// Stat before reading. We want a distinct file_not_found
// error so the server can decide whether to retry (network
// mount slow to converge) or surface to the operator.
info, err := fsys.IO.Stat(p.Path)
// Use the canonical (symlink-resolved) path — NOT the raw
// p.Path — to close the TOCTOU window where a symlink
// swap between checkAllowed and Stat would bypass the
// allowlist.
info, err := fsys.IO.Stat(canonical)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
fsys.audit("read", canonical, "error", 0)
Expand All @@ -220,7 +224,7 @@ func (fsys *FileSystem) ReadHandler(ctx context.Context, requestID string, paylo
}), nil
}

data, err := fsys.IO.ReadFile(p.Path)
data, err := fsys.IO.ReadFile(canonical)
if err != nil {
fsys.audit("read", canonical, "error", 0)
return NewReadResultEnvelope(requestID, ReadResultPayload{
Expand Down Expand Up @@ -337,7 +341,7 @@ func (fsys *FileSystem) WriteHandler(ctx context.Context, requestID string, payl
// create a directory tree — a write to a non-existent
// parent returns ENOENT and surfaces as io_error.
const filePerm os.FileMode = 0o644
n, err := fsys.IO.WriteFile(p.Path, data, filePerm, mode)
n, err := fsys.IO.WriteFile(canonical, data, filePerm, mode)
if err != nil {
fsys.audit("write", canonical, "error", int64(0))
errCode := "io_error"
Expand Down
7 changes: 0 additions & 7 deletions internal/wire/reconnect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ package wire
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"sync"
Expand Down Expand Up @@ -675,9 +674,3 @@ func containsSubstr(haystack, needle string) bool {
}
return false
}

// Ensure unused imports are referenced (the linter complains
// about fmt otherwise). These are intentional — fmt is used in
// the test rig's URL builders and errors is used in the
// dispatch-panic test.
var _ = fmt.Sprintf
Loading