More paste fixes
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/andydixon/nexus/internal/terminalenv"
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,16 +87,13 @@ func (c *Client) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(c.shell)
|
cmd := exec.Command(c.shell)
|
||||||
cmd.Env = os.Environ()
|
cmd.Env = terminalenv.LocalShellEnv(os.Environ())
|
||||||
|
|
||||||
// Start in the user's home directory.
|
// Start in the user's home directory.
|
||||||
if home, err := os.UserHomeDir(); err == nil {
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
cmd.Dir = home
|
cmd.Dir = home
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set TERM for proper terminal behaviour.
|
|
||||||
cmd.Env = append(cmd.Env, "TERM=xterm-256color")
|
|
||||||
|
|
||||||
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{
|
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{
|
||||||
Cols: c.cols,
|
Cols: c.cols,
|
||||||
Rows: c.rows,
|
Rows: c.rows,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/andydixon/nexus/internal/models"
|
"github.com/andydixon/nexus/internal/models"
|
||||||
|
"github.com/andydixon/nexus/internal/terminalenv"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
@@ -164,6 +165,14 @@ func (c *Client) connectInternal(
|
|||||||
ssh.IUTF8: 1,
|
ssh.IUTF8: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionEnv := terminalenv.SSHEnvVars(sc.EnvVars)
|
||||||
|
for _, key := range terminalenv.SortedEnvKeys(sessionEnv) {
|
||||||
|
// Servers may reject env requests unless AcceptEnv permits them. Locale
|
||||||
|
// requests improve Unicode input when accepted, but rejection should not
|
||||||
|
// prevent the terminal session from starting.
|
||||||
|
_ = session.Setenv(key, sessionEnv[key])
|
||||||
|
}
|
||||||
|
|
||||||
if err := session.RequestPty(termType, rows, cols, modes); err != nil {
|
if err := session.RequestPty(termType, rows, cols, modes); err != nil {
|
||||||
session.Close()
|
session.Close()
|
||||||
sshConn.Close()
|
sshConn.Close()
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
// Package terminalenv prepares environment variables for terminal sessions.
|
||||||
|
package terminalenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
envLANG = "LANG"
|
||||||
|
envLCAll = "LC_ALL"
|
||||||
|
envLCType = "LC_CTYPE"
|
||||||
|
envTerm = "TERM"
|
||||||
|
defaultTerm = "xterm-256color"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocalShellEnv returns an environment suitable for a local interactive PTY.
|
||||||
|
func LocalShellEnv(env []string) []string {
|
||||||
|
env = setEnv(env, envTerm, defaultTerm)
|
||||||
|
return ensureUTF8CharacterLocale(env, runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSHEnvVars returns environment variables to request for an SSH session.
|
||||||
|
// Profile locale values are preserved; otherwise a UTF-8 character locale is
|
||||||
|
// requested by default.
|
||||||
|
func SSHEnvVars(profileEnv map[string]string) map[string]string {
|
||||||
|
result := map[string]string{}
|
||||||
|
for key, value := range profileEnv {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasAnyLocaleMap(result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result[envLCType] = DefaultUTF8Locale(runtime.GOOS)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortedEnvKeys returns stable keys for deterministic environment requests.
|
||||||
|
func SortedEnvKeys(env map[string]string) []string {
|
||||||
|
keys := make([]string, 0, len(env))
|
||||||
|
for key := range env {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultUTF8Locale returns a locale name broadly available on the target OS.
|
||||||
|
func DefaultUTF8Locale(goos string) string {
|
||||||
|
if goos == "darwin" {
|
||||||
|
return "en_US.UTF-8"
|
||||||
|
}
|
||||||
|
return "C.UTF-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureUTF8CharacterLocale(env []string, goos string) []string {
|
||||||
|
if hasEffectiveUTF8Locale(env) {
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
locale := DefaultUTF8Locale(goos)
|
||||||
|
if hasEnv(env, envLCAll) {
|
||||||
|
return setEnv(env, envLCAll, locale)
|
||||||
|
}
|
||||||
|
if hasEnv(env, envLCType) {
|
||||||
|
return setEnv(env, envLCType, locale)
|
||||||
|
}
|
||||||
|
if hasEnv(env, envLANG) {
|
||||||
|
return setEnv(env, envLCType, locale)
|
||||||
|
}
|
||||||
|
return setEnv(env, envLANG, locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasEffectiveUTF8Locale(env []string) bool {
|
||||||
|
values := envToMap(env)
|
||||||
|
if value, ok := values[envLCAll]; ok {
|
||||||
|
return isUTF8Locale(value)
|
||||||
|
}
|
||||||
|
if value, ok := values[envLCType]; ok {
|
||||||
|
return isUTF8Locale(value)
|
||||||
|
}
|
||||||
|
if value, ok := values[envLANG]; ok {
|
||||||
|
return isUTF8Locale(value)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAnyLocaleMap(env map[string]string) bool {
|
||||||
|
_, hasLCAll := env[envLCAll]
|
||||||
|
_, hasLCType := env[envLCType]
|
||||||
|
_, hasLANG := env[envLANG]
|
||||||
|
return hasLCAll || hasLCType || hasLANG
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUTF8Locale(value string) bool {
|
||||||
|
normalized := strings.ToLower(strings.ReplaceAll(value, "-", ""))
|
||||||
|
return strings.Contains(normalized, "utf8")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setEnv(env []string, key, value string) []string {
|
||||||
|
prefix := key + "="
|
||||||
|
result := make([]string, 0, len(env)+1)
|
||||||
|
for _, item := range env {
|
||||||
|
if strings.HasPrefix(item, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
return append(result, prefix+value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasEnv(env []string, key string) bool {
|
||||||
|
prefix := key + "="
|
||||||
|
for _, item := range env {
|
||||||
|
if strings.HasPrefix(item, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func envToMap(env []string) map[string]string {
|
||||||
|
result := map[string]string{}
|
||||||
|
for _, item := range env {
|
||||||
|
key, value, ok := strings.Cut(item, "=")
|
||||||
|
if ok {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package terminalenv
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLocalShellEnvSetsTermAndUTF8WhenMissing(t *testing.T) {
|
||||||
|
got := LocalShellEnv([]string{"SHELL=/bin/zsh"})
|
||||||
|
|
||||||
|
if value := envValue(got, envTerm); value != defaultTerm {
|
||||||
|
t.Fatalf("TERM = %q, want %q", value, defaultTerm)
|
||||||
|
}
|
||||||
|
if value := envValue(got, envLANG); value == "" || !isUTF8Locale(value) {
|
||||||
|
t.Fatalf("LANG = %q, want UTF-8 locale", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalShellEnvUsesLCTypeWhenLangIsNonUTF8(t *testing.T) {
|
||||||
|
got := ensureUTF8CharacterLocale([]string{"LANG=C"}, "linux")
|
||||||
|
|
||||||
|
if value := envValue(got, envLANG); value != "C" {
|
||||||
|
t.Fatalf("LANG = %q, want C", value)
|
||||||
|
}
|
||||||
|
if value := envValue(got, envLCType); value != "C.UTF-8" {
|
||||||
|
t.Fatalf("LC_CTYPE = %q, want C.UTF-8", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalShellEnvOverridesNonUTF8LCAll(t *testing.T) {
|
||||||
|
got := ensureUTF8CharacterLocale([]string{"LC_ALL=C", "LANG=C"}, "darwin")
|
||||||
|
|
||||||
|
if value := envValue(got, envLCAll); value != "en_US.UTF-8" {
|
||||||
|
t.Fatalf("LC_ALL = %q, want en_US.UTF-8", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSHEnvVarsPreservesProfileUTF8Locale(t *testing.T) {
|
||||||
|
got := SSHEnvVars(map[string]string{
|
||||||
|
"LANG": "de_DE.UTF-8",
|
||||||
|
"FOO": "bar",
|
||||||
|
})
|
||||||
|
|
||||||
|
if got["LANG"] != "de_DE.UTF-8" {
|
||||||
|
t.Fatalf("LANG = %q, want profile value", got["LANG"])
|
||||||
|
}
|
||||||
|
if got["FOO"] != "bar" {
|
||||||
|
t.Fatalf("FOO = %q, want bar", got["FOO"])
|
||||||
|
}
|
||||||
|
if _, ok := got[envLCType]; ok {
|
||||||
|
t.Fatalf("unexpected LC_CTYPE default when LANG is already UTF-8")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSHEnvVarsPreservesProfileNonUTF8Locale(t *testing.T) {
|
||||||
|
got := SSHEnvVars(map[string]string{
|
||||||
|
envLANG: "C",
|
||||||
|
"FOO": "bar",
|
||||||
|
})
|
||||||
|
|
||||||
|
if got[envLANG] != "C" {
|
||||||
|
t.Fatalf("LANG = %q, want C", got[envLANG])
|
||||||
|
}
|
||||||
|
if _, ok := got[envLCType]; ok {
|
||||||
|
t.Fatalf("unexpected LC_CTYPE default when profile sets locale")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSHEnvVarsAddsUTF8LCTypeByDefault(t *testing.T) {
|
||||||
|
got := SSHEnvVars(map[string]string{"FOO": "bar"})
|
||||||
|
|
||||||
|
if got["FOO"] != "bar" {
|
||||||
|
t.Fatalf("FOO = %q, want bar", got["FOO"])
|
||||||
|
}
|
||||||
|
if value := got[envLCType]; value == "" || !isUTF8Locale(value) {
|
||||||
|
t.Fatalf("LC_CTYPE = %q, want UTF-8 locale", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func envValue(env []string, key string) string {
|
||||||
|
prefix := key + "="
|
||||||
|
for _, item := range env {
|
||||||
|
if len(item) >= len(prefix) && item[:len(prefix)] == prefix {
|
||||||
|
return item[len(prefix):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user