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
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/coder/websocket v1.8.14
github.com/confiant-inc/go-socks5 v0.0.0-20210816151940-c1124825b1d6
github.com/creack/pty v1.1.24
github.com/gin-gonic/gin v1.11.0
github.com/gin-gonic/gin v1.12.0
github.com/go-chi/chi/v5 v5.2.5
github.com/go-redis/redis/v8 v8.11.5
github.com/hashicorp/yamux v0.1.2
Expand All @@ -23,7 +23,7 @@ require (
github.com/skycoin/skywire v1.3.35-0.20260222235451-f11c46808634
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
golang.org/x/net v0.50.0
golang.org/x/net v0.51.0
golang.org/x/sys v0.41.0
golang.org/x/term v0.40.0
)
Expand Down Expand Up @@ -87,6 +87,7 @@ require (
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
Expand Down
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
Expand Down Expand Up @@ -204,6 +204,8 @@ github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OL
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
github.com/xtaci/smux v1.5.56 h1:Eyv/dUULmkGZZNucLUisnkzJ/4UQ5YZTschhugFBM0U=
github.com/xtaci/smux v1.5.56/go.mod h1:IGQ9QYrBphmb/4aTnLEcJby0TNr3NV+OslIOMrX825Q=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
Expand Down Expand Up @@ -231,8 +233,8 @@ golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200428200454-593003d681fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
33 changes: 32 additions & 1 deletion pkg/dmsgpty/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ func (cli *CLI) servePty(ctx context.Context, ptyC *PtyClient, cmd string, args
WithField("cmd", fmt.Sprint(append([]string{cmd}, args...))).
Debugf("Executing...")

if err := ptyC.Start(cmd, args...); err != nil {
// Capture essential environment variables from the client to pass to the remote PTY
env := cli.captureEnv()

if err := ptyC.Start(cmd, env, args...); err != nil {
return fmt.Errorf("failed to start command on pty: %v", err)
}

Expand Down Expand Up @@ -152,3 +155,31 @@ func (cli *CLI) servePty(ctx context.Context, ptyC *PtyClient, cmd string, args

return nil
}

// captureEnv captures essential environment variables from the client to pass to the remote PTY.
// This ensures the remote shell has proper terminal settings.
func (cli *CLI) captureEnv() []string {
// List of environment variables to pass from client to remote PTY
envVars := []string{"TERM", "COLORTERM", "LANG", "LC_ALL"}
var env []string

for _, name := range envVars {
if val := os.Getenv(name); val != "" {
env = append(env, name+"="+val)
}
}

// If TERM is not set, default to a sensible value
hasTerm := false
for _, e := range env {
if strings.HasPrefix(e, "TERM=") {
hasTerm = true
break
}
}
if !hasTerm {
env = append(env, "TERM=xterm-256color")
}

return env
}
4 changes: 2 additions & 2 deletions pkg/dmsgpty/host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,9 @@ func tempWhitelist(t *testing.T, c *dmsg.Client) (Whitelist, func()) {

func checkPty(t *testing.T, ptyC *PtyClient, msg string) {
if runtime.GOOS == "windows" {
require.NoError(t, ptyC.Start(DefaultCmd, "-Command", "Write-Host "+msg))
require.NoError(t, ptyC.Start(DefaultCmd, nil, "-Command", "Write-Host "+msg))
} else {
require.NoError(t, ptyC.Start(DefaultCmd, "-c", "echo "+msg))
require.NoError(t, ptyC.Start(DefaultCmd, nil, "-c", "echo "+msg))
}

readB := make([]byte, len(msg))
Expand Down
11 changes: 6 additions & 5 deletions pkg/dmsgpty/pty_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,19 @@ func (sc *PtyClient) call(method string, args, reply interface{}) error {
}
}

// Start starts the pty.
func (sc *PtyClient) Start(name string, arg ...string) error {
// Start starts the pty with optional environment variables.
func (sc *PtyClient) Start(name string, env []string, arg ...string) error {
return sc.call("Start", &CommandReq{
Name: name,
Arg: arg,
Size: nil,
Env: env,
}, &empty)
}

// StartWithSize starts the pty with a specified size.
func (sc *PtyClient) StartWithSize(name string, arg []string, c *WinSize) error {
return sc.call("Start", &CommandReq{Name: name, Arg: arg, Size: c}, &empty)
// StartWithSize starts the pty with a specified size and optional environment variables.
func (sc *PtyClient) StartWithSize(name string, arg []string, c *WinSize, env []string) error {
return sc.call("Start", &CommandReq{Name: name, Arg: arg, Size: c, Env: env}, &empty)
}

// SetPtySize sets the pty size.
Expand Down
5 changes: 3 additions & 2 deletions pkg/dmsgpty/pty_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type CommandReq struct {
Name string
Arg []string
Size *WinSize
Env []string // Environment variables to pass to the PTY (e.g., TERM=xterm-256color)
}

// LocalPtyGateway is the gateway to a local pty.
Expand Down Expand Up @@ -50,7 +51,7 @@ func (g *LocalPtyGateway) Read(reqN *int, respB *[]byte) error {

// Start starts the local pty.
func (g *LocalPtyGateway) Start(req *CommandReq, _ *struct{}) error {
return g.ses.Start(req.Name, req.Arg, req.Size)
return g.ses.Start(req.Name, req.Arg, req.Size, req.Env)
}

// Write writes to the local pty.
Expand Down Expand Up @@ -82,7 +83,7 @@ func NewProxyGateway(ptyC *PtyClient) PtyGateway {

// Start starts the remote pty.
func (g *ProxiedPtyGateway) Start(req *CommandReq, _ *struct{}) error {
return g.ptyC.Start(req.Name, req.Arg...)
return g.ptyC.Start(req.Name, req.Env, req.Arg...)
}

// Stop stops the remote pty.
Expand Down
40 changes: 37 additions & 3 deletions pkg/dmsgpty/pty_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"os"
"os/exec"
"strings"
"sync"

"github.com/creack/pty"
Expand Down Expand Up @@ -68,8 +69,9 @@ func (s *Pty) Write(b []byte) (int, error) {
return s.pty.Write(b)
}

// Start runs a command with the given command name, args and optional window size.
func (s *Pty) Start(name string, args []string, size *WinSize) error {
// Start runs a command with the given command name, args, optional window size, and optional environment variables.
// If env is provided, those variables will be merged with (and override) the host's environment.
func (s *Pty) Start(name string, args []string, size *WinSize, env []string) error {
s.mx.Lock()
defer s.mx.Unlock()

Expand All @@ -78,7 +80,7 @@ func (s *Pty) Start(name string, args []string, size *WinSize) error {
}

cmd := exec.Command(name, args...) //nolint:gosec
cmd.Env = os.Environ()
cmd.Env = mergeEnv(os.Environ(), env)
var sz *pty.Winsize
var err error

Expand All @@ -97,6 +99,38 @@ func (s *Pty) Start(name string, args []string, size *WinSize) error {
return nil
}

// mergeEnv merges the base environment with override variables.
// Variables in override will replace any matching variables in base.
func mergeEnv(base, override []string) []string {
if len(override) == 0 {
return base
}

// Build a map of override variables for quick lookup
overrideMap := make(map[string]string)
for _, e := range override {
if idx := strings.Index(e, "="); idx > 0 {
overrideMap[e[:idx]] = e
}
}

// Filter base, keeping only variables not in override
result := make([]string, 0, len(base)+len(override))
for _, e := range base {
if idx := strings.Index(e, "="); idx > 0 {
key := e[:idx]
if _, exists := overrideMap[key]; !exists {
result = append(result, e)
}
}
}

// Add all override variables
result = append(result, override...)

return result
}

// SetPtySize sets the pty size.
func (s *Pty) SetPtySize(size *WinSize) error {
s.mx.RLock()
Expand Down
40 changes: 37 additions & 3 deletions pkg/dmsgpty/pty_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package dmsgpty
import (
"errors"
"os"
"strings"
"sync"
"syscall"

Expand Down Expand Up @@ -69,8 +70,9 @@ func (s *Pty) Write(b []byte) (int, error) {
return int(res), err
}

// Start runs a command with the given command name, args and optional window size.
func (s *Pty) Start(name string, args []string, size *WinSize) error {
// Start runs a command with the given command name, args, optional window size, and optional environment variables.
// If env is provided, those variables will be merged with (and override) the host's environment.
func (s *Pty) Start(name string, args []string, size *WinSize, env []string) error {
s.mx.Lock()
defer s.mx.Unlock()

Expand Down Expand Up @@ -99,7 +101,7 @@ func (s *Pty) Start(name string, args []string, size *WinSize) error {
name,
args,
&syscall.ProcAttr{
Env: os.Environ(),
Env: mergeEnv(os.Environ(), env),
},
)

Expand All @@ -111,6 +113,38 @@ func (s *Pty) Start(name string, args []string, size *WinSize) error {
return nil
}

// mergeEnv merges the base environment with override variables.
// Variables in override will replace any matching variables in base.
func mergeEnv(base, override []string) []string {
if len(override) == 0 {
return base
}

// Build a map of override variables for quick lookup
overrideMap := make(map[string]string)
for _, e := range override {
if idx := strings.Index(e, "="); idx > 0 {
overrideMap[e[:idx]] = e
}
}

// Filter base, keeping only variables not in override
result := make([]string, 0, len(base)+len(override))
for _, e := range base {
if idx := strings.Index(e, "="); idx > 0 {
key := e[:idx]
if _, exists := overrideMap[key]; !exists {
result = append(result, e)
}
}
}

// Add all override variables
result = append(result, override...)

return result
}

// SetPtySize sets the pty size.
func (s *Pty) SetPtySize(size *WinSize) error {
s.mx.RLock()
Expand Down
Loading
Loading