feat(ssh): support proxy command connections

This commit is contained in:
陈奇
2026-06-05 06:03:45 +00:00
parent 65cd8aba79
commit 099beb8438
27 changed files with 541 additions and 102 deletions

View File

@@ -2,7 +2,7 @@ import type { SftpFilenameEncoding } from './sftp';
import type { KeywordHighlightRule } from './terminal';
// Proxy configuration for SSH connections
type ProxyType = 'http' | 'socks5';
type ProxyType = 'http' | 'socks5' | 'command';
// UI locale identifier, stored in settings and used for i18n (e.g., "en", "zh-CN").
export type UILanguage = string;
@@ -10,6 +10,7 @@ export interface ProxyConfig {
type: ProxyType;
host: string;
port: number;
command?: string;
username?: string;
password?: string;
}

View File

@@ -84,8 +84,40 @@ test("normalizeManualProxyConfig clears empty proxy drafts", () => {
);
});
test("normalizeManualProxyConfig trims command proxy drafts", () => {
assert.deepEqual(
normalizeManualProxyConfig({
type: "command",
host: "ignored.example.com",
port: 8080,
command: " cloudflared access ssh --hostname %h ",
username: "ignored",
password: "ignored",
}),
{
type: "command",
host: "",
port: 0,
command: "cloudflared access ssh --hostname %h",
},
);
});
test("isCompleteProxyConfig requires host and a valid port", () => {
assert.equal(isCompleteProxyConfig({ type: "http", host: "", port: 8080 }), false);
assert.equal(isCompleteProxyConfig({ type: "http", host: "proxy.example.com", port: 0 }), false);
assert.equal(isCompleteProxyConfig({ type: "http", host: "proxy.example.com", port: 3128 }), true);
});
test("isCompleteProxyConfig accepts a non-empty command proxy", () => {
assert.equal(isCompleteProxyConfig({ type: "command", host: "", port: 0, command: "" }), false);
assert.equal(
isCompleteProxyConfig({
type: "command",
host: "",
port: 0,
command: "cloudflared access ssh --hostname %h",
}),
true,
);
});

View File

@@ -9,12 +9,18 @@ export const isValidProxyPort = (port: unknown): boolean => {
return Number.isInteger(value) && value >= 1 && value <= 65535;
};
export const isProxyCommandConfig = (config: ProxyConfig | undefined): boolean => {
return config?.type === "command";
};
export const isEmptyProxyConfigDraft = (config: ProxyConfig | undefined): boolean => {
if (!config) return true;
if (isProxyCommandConfig(config)) return !config.command?.trim();
return !config.host.trim() && !config.username?.trim() && !config.password?.trim();
};
export const isCompleteProxyConfig = (config: ProxyConfig | undefined): boolean => {
if (isProxyCommandConfig(config)) return Boolean(config?.command?.trim());
return Boolean(config?.host.trim()) && isValidProxyPort(config?.port);
};
@@ -22,6 +28,14 @@ export const normalizeManualProxyConfig = (
config: ProxyConfig | undefined,
): ProxyConfig | undefined => {
if (!config || isEmptyProxyConfigDraft(config)) return undefined;
if (isProxyCommandConfig(config)) {
return {
type: "command",
host: "",
port: 0,
command: config.command?.trim(),
};
}
return {
...config,
host: config.host.trim(),
@@ -30,6 +44,16 @@ export const normalizeManualProxyConfig = (
};
};
export const hasUsableProxyConfig = (config: ProxyConfig | undefined): boolean => {
return isCompleteProxyConfig(config);
};
export const formatProxyConfigEndpoint = (config: ProxyConfig | undefined): string => {
if (!config) return "";
if (isProxyCommandConfig(config)) return config.command?.trim() || "";
return `${config.host}:${config.port}`;
};
export function findProxyProfile(
proxyProfileId: string | undefined,
proxyProfiles: ProxyProfile[],