Skip to content

Commit e1b6080

Browse files
committed
fix: allow non-ASCII characters in SSH host aliases
The userHostRe regexp only accepted ASCII letters and digits in the host portion, rejecting valid SSH config Host aliases that contain Chinese or other Unicode characters (e.g. "PROD-服务器"). Widen the character classes with \p{L} and \p{N} to accept Unicode letters and digits. Fixes #2937 Signed-off-by: majiayu000 <1835304752@qq.com>
1 parent 96c2526 commit e1b6080

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

pkg/remote/connutil.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
"golang.org/x/crypto/ssh"
2727
)
2828

29-
var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9._@\\-]*@)?([a-zA-Z0-9][a-zA-Z0-9.-]*)(?::([0-9]+))?$`)
29+
var userHostRe = regexp.MustCompile(`^([a-zA-Z0-9\p{L}\p{N}][a-zA-Z0-9._@\p{L}\p{N}\\-]*@)?([a-zA-Z0-9\p{L}\p{N}][a-zA-Z0-9.\p{L}\p{N}-]*)(?::([0-9]+))?$`)
3030

3131
func ParseOpts(input string) (*SSHOpts, error) {
3232
m := userHostRe.FindStringSubmatch(input)

pkg/remote/connutil_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package remote
5+
6+
import (
7+
"testing"
8+
)
9+
10+
func TestParseOpts(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
input string
14+
wantUser string
15+
wantHost string
16+
wantPort string
17+
wantErr bool
18+
}{
19+
{"user@host:port", "user@myserver:22", "user", "myserver", "22", false},
20+
{"host only", "myserver", "", "myserver", "", false},
21+
{"chinese host alias", "PROD-服务器", "", "PROD-服务器", "", false},
22+
{"mixed ascii and chinese with user and port", "user@PROD-阿里云:22", "user", "PROD-阿里云", "22", false},
23+
{"unicode only host", "服务器", "", "服务器", "", false},
24+
{"japanese host", "サーバー", "", "サーバー", "", false},
25+
{"empty string", "", "", "", "", true},
26+
{"just colon", ":", "", "", "", true},
27+
{"just at", "@", "", "", "", true},
28+
}
29+
for _, tt := range tests {
30+
t.Run(tt.name, func(t *testing.T) {
31+
opts, err := ParseOpts(tt.input)
32+
if tt.wantErr {
33+
if err == nil {
34+
t.Fatalf("expected error, got nil")
35+
}
36+
return
37+
}
38+
if err != nil {
39+
t.Fatalf("unexpected error: %v", err)
40+
}
41+
if opts.SSHUser != tt.wantUser {
42+
t.Errorf("user: got %q, want %q", opts.SSHUser, tt.wantUser)
43+
}
44+
if opts.SSHHost != tt.wantHost {
45+
t.Errorf("host: got %q, want %q", opts.SSHHost, tt.wantHost)
46+
}
47+
if opts.SSHPort != tt.wantPort {
48+
t.Errorf("port: got %q, want %q", opts.SSHPort, tt.wantPort)
49+
}
50+
})
51+
}
52+
}

0 commit comments

Comments
 (0)