fix: improve Windows tunnel support
This commit is contained in:
@@ -149,6 +149,84 @@ interface TunnelProviderCapability {
|
||||
modes?: TunnelProviderModeDescriptor[];
|
||||
}
|
||||
|
||||
interface TunnelCheckResponse {
|
||||
available?: boolean;
|
||||
provider?: string | null;
|
||||
version?: string | null;
|
||||
dependency?: string | null;
|
||||
installCommand?: string | null;
|
||||
platform?: string | null;
|
||||
}
|
||||
|
||||
interface TunnelDependencyInstallInfo {
|
||||
provider: string;
|
||||
dependency: string;
|
||||
installCommand: string;
|
||||
}
|
||||
|
||||
const getProviderDependencyName = (provider: string): string => (provider === 'ngrok' ? 'ngrok' : 'cloudflared');
|
||||
|
||||
const getClientInstallPlatform = (): string => {
|
||||
if (typeof window !== 'undefined' && typeof window.__OPENCHAMBER_PLATFORM__ === 'string') {
|
||||
const platform = window.__OPENCHAMBER_PLATFORM__;
|
||||
if (platform === 'win32' || platform === 'darwin' || platform === 'linux') {
|
||||
return platform;
|
||||
}
|
||||
}
|
||||
|
||||
const browserPlatform = typeof navigator !== 'undefined'
|
||||
? `${navigator.platform || ''} ${navigator.userAgent || ''}`.toLowerCase()
|
||||
: '';
|
||||
if (browserPlatform.includes('win')) {
|
||||
return 'win32';
|
||||
}
|
||||
if (browserPlatform.includes('mac')) {
|
||||
return 'darwin';
|
||||
}
|
||||
return 'linux';
|
||||
};
|
||||
|
||||
const getFallbackInstallCommand = (provider: string, platform = getClientInstallPlatform()): string => {
|
||||
if (provider === 'ngrok') {
|
||||
if (platform === 'win32') {
|
||||
return 'winget install ngrok -s msstore';
|
||||
}
|
||||
if (platform === 'darwin') {
|
||||
return 'brew install ngrok';
|
||||
}
|
||||
return 'https://ngrok.com/download';
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
return 'winget install --id Cloudflare.cloudflared';
|
||||
}
|
||||
if (platform === 'darwin') {
|
||||
return 'brew install cloudflared';
|
||||
}
|
||||
return 'https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflared/downloads/';
|
||||
};
|
||||
|
||||
const createTunnelDependencyInstallInfo = (provider: string, checkData?: TunnelCheckResponse): TunnelDependencyInstallInfo => {
|
||||
const responseProvider = typeof checkData?.provider === 'string' && checkData.provider.trim().length > 0
|
||||
? checkData.provider.trim().toLowerCase()
|
||||
: provider;
|
||||
const dependency = typeof checkData?.dependency === 'string' && checkData.dependency.trim().length > 0
|
||||
? checkData.dependency.trim()
|
||||
: getProviderDependencyName(responseProvider);
|
||||
const platform = typeof checkData?.platform === 'string' && checkData.platform.trim().length > 0
|
||||
? checkData.platform.trim()
|
||||
: getClientInstallPlatform();
|
||||
const installCommand = typeof checkData?.installCommand === 'string' && checkData.installCommand.trim().length > 0
|
||||
? checkData.installCommand.trim()
|
||||
: getFallbackInstallCommand(responseProvider, platform);
|
||||
|
||||
return {
|
||||
provider: responseProvider,
|
||||
dependency,
|
||||
installCommand,
|
||||
};
|
||||
};
|
||||
|
||||
const getProviderLabel = (provider: string): string => {
|
||||
if (provider === 'cloudflare') {
|
||||
return 'Cloudflare';
|
||||
@@ -281,6 +359,7 @@ export const TunnelSettings: React.FC = () => {
|
||||
const [isSavingTtl, setIsSavingTtl] = React.useState(false);
|
||||
const [isSavingMode, setIsSavingMode] = React.useState(false);
|
||||
const [tunnelProvider, setTunnelProvider] = React.useState<string>('cloudflare');
|
||||
const [dependencyInstallInfo, setDependencyInstallInfo] = React.useState<TunnelDependencyInstallInfo>(() => createTunnelDependencyInstallInfo('cloudflare'));
|
||||
const [providerCapabilities, setProviderCapabilities] = React.useState<TunnelProviderCapability[]>([]);
|
||||
const [tunnelMode, setTunnelMode] = React.useState<TunnelMode>('quick');
|
||||
const [managedLocalConfigPath, setManagedLocalConfigPath] = React.useState<string | null>(null);
|
||||
@@ -400,13 +479,47 @@ export const TunnelSettings: React.FC = () => {
|
||||
() => tunnelModeOptions.some((option) => option.value === 'managed-remote' || option.value === 'managed-local'),
|
||||
[tunnelModeOptions],
|
||||
);
|
||||
const installCommand = tunnelProvider === 'ngrok'
|
||||
? 'brew install ngrok'
|
||||
: 'brew install cloudflared';
|
||||
const displayedDependencyInstallInfo = React.useMemo(() => {
|
||||
if (dependencyInstallInfo.provider === tunnelProvider) {
|
||||
return dependencyInstallInfo;
|
||||
}
|
||||
return createTunnelDependencyInstallInfo(tunnelProvider);
|
||||
}, [dependencyInstallInfo, tunnelProvider]);
|
||||
const openExternal = React.useCallback(async (url: string) => {
|
||||
await openExternalUrl(url);
|
||||
}, []);
|
||||
|
||||
const applyDependencyCheck = React.useCallback((checkData: TunnelCheckResponse, fallbackProvider: string): boolean => {
|
||||
setDependencyInstallInfo(createTunnelDependencyInstallInfo(fallbackProvider, checkData));
|
||||
return checkData.available === true;
|
||||
}, []);
|
||||
|
||||
const refreshTunnelDependencyCheck = React.useCallback(async (provider: string, signal?: AbortSignal): Promise<boolean | null> => {
|
||||
try {
|
||||
const checkRes = await runtimeFetch('/api/openchamber/tunnel/check', {
|
||||
query: { provider },
|
||||
...(signal ? { signal } : {}),
|
||||
});
|
||||
if (!checkRes.ok) {
|
||||
return null;
|
||||
}
|
||||
const checkData = (await checkRes.json()) as TunnelCheckResponse;
|
||||
if (signal?.aborted) {
|
||||
return null;
|
||||
}
|
||||
const available = applyDependencyCheck(checkData, provider);
|
||||
setState((current) => {
|
||||
if (current === 'checking' || current === 'starting' || current === 'active' || current === 'stopping') {
|
||||
return current;
|
||||
}
|
||||
return available ? 'idle' : 'not-available';
|
||||
});
|
||||
return available;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [applyDependencyCheck]);
|
||||
|
||||
const checkAvailabilityAndStatus = React.useCallback(async (signal: AbortSignal) => {
|
||||
try {
|
||||
const [checkRes, statusRes, settingsRes, providersRes] = await Promise.all([
|
||||
@@ -416,7 +529,7 @@ export const TunnelSettings: React.FC = () => {
|
||||
runtimeFetch('/api/openchamber/tunnel/providers', { signal }),
|
||||
]);
|
||||
|
||||
const checkData = await checkRes.json();
|
||||
const checkData = (await checkRes.json()) as TunnelCheckResponse;
|
||||
const statusData = (await statusRes.json()) as TunnelStatusResponse;
|
||||
const settingsData = settingsRes.ok ? await settingsRes.json() : {};
|
||||
const providersData = providersRes.ok ? await providersRes.json() : {};
|
||||
@@ -440,6 +553,7 @@ export const TunnelSettings: React.FC = () => {
|
||||
const loadedManagedLocalConfigPath = typeof settingsData?.managedLocalTunnelConfigPath === 'string'
|
||||
? settingsData.managedLocalTunnelConfigPath.trim() || null
|
||||
: null;
|
||||
const dependencyAvailable = applyDependencyCheck(checkData, loadedProvider);
|
||||
|
||||
const loadedPresetsFromStatus = sanitizePresets(statusData?.managedRemoteTunnelPresets);
|
||||
const loadedHostname = typeof statusData.managedRemoteTunnelHostname === 'string'
|
||||
@@ -484,14 +598,14 @@ export const TunnelSettings: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(checkData.available ? 'idle' : 'not-available');
|
||||
setState(dependencyAvailable ? 'idle' : 'not-available');
|
||||
} catch {
|
||||
if (!signal.aborted) {
|
||||
setState('error');
|
||||
setErrorMessage(t('settings.openchamber.tunnel.toast.checkAvailabilityFailed'));
|
||||
}
|
||||
}
|
||||
}, [t]);
|
||||
}, [applyDependencyCheck, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
@@ -738,7 +852,8 @@ export const TunnelSettings: React.FC = () => {
|
||||
? tunnelMode
|
||||
: toUiTunnelMode(capability?.modes?.[0]?.key);
|
||||
await saveTunnelSettings({ tunnelProvider: provider, tunnelMode: defaultMode });
|
||||
}, [providerCapabilities, saveTunnelSettings, tunnelMode]);
|
||||
void refreshTunnelDependencyCheck(provider);
|
||||
}, [providerCapabilities, refreshTunnelDependencyCheck, saveTunnelSettings, tunnelMode]);
|
||||
|
||||
const handleBrowseManagedLocalConfig = React.useCallback(async () => {
|
||||
const result = await requestFileAccess({
|
||||
@@ -1192,10 +1307,12 @@ export const TunnelSettings: React.FC = () => {
|
||||
<div className="flex items-start gap-2 rounded-lg border border-[var(--status-warning)]/30 bg-[var(--status-warning)]/5 p-3">
|
||||
<Icon name="error-warning" className="mt-0.5 size-4 shrink-0 text-[var(--status-warning)]" />
|
||||
<div className="space-y-1">
|
||||
<p className="typography-meta font-medium text-foreground">{getProviderLabel(tunnelProvider)} tunnel dependency was not found.</p>
|
||||
<p className="typography-meta font-medium text-foreground">
|
||||
{t('settings.openchamber.tunnel.notAvailable.dependencyNotFound', { dependency: displayedDependencyInstallInfo.dependency })}
|
||||
</p>
|
||||
<p className="typography-meta text-muted-foreground/70">{t('settings.openchamber.tunnel.notAvailable.installHint')}</p>
|
||||
<code className="typography-code block rounded bg-muted/50 px-2 py-1 text-xs text-foreground">
|
||||
{installCommand}
|
||||
{displayedDependencyInstallInfo.installCommand}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -119,6 +119,7 @@ export const settingsDict = {
|
||||
'settings.openchamber.tunnel.section.redeemedAccessLinks': 'Redeemed access links',
|
||||
'settings.openchamber.tunnel.section.savedManagedRemoteTunnels': 'Saved managed remote tunnels',
|
||||
'settings.openchamber.tunnel.notAvailable.cloudflaredNotFound': 'cloudflared not found',
|
||||
'settings.openchamber.tunnel.notAvailable.dependencyNotFound': '{dependency} was not found.',
|
||||
'settings.openchamber.tunnel.notAvailable.installHint': 'Install it to enable remote tunnel access:',
|
||||
'settings.openchamber.tunnel.field.provider': 'Provider',
|
||||
'settings.openchamber.tunnel.field.providerPlaceholder': 'Select provider',
|
||||
|
||||
@@ -86,6 +86,7 @@ export const settingsDict = {
|
||||
"settings.openchamber.tunnel.section.redeemedAccessLinks": "Enlaces de acceso canjeados",
|
||||
"settings.openchamber.tunnel.section.savedManagedRemoteTunnels": "Túneles remotos gestionados guardados",
|
||||
"settings.openchamber.tunnel.notAvailable.cloudflaredNotFound": "cloudflared no encontrado",
|
||||
"settings.openchamber.tunnel.notAvailable.dependencyNotFound": "No se encontró {dependency}.",
|
||||
"settings.openchamber.tunnel.notAvailable.installHint": "Instálalo para habilitar el acceso al túnel remoto:",
|
||||
"settings.openchamber.tunnel.field.provider": "Proveedor",
|
||||
"settings.openchamber.tunnel.field.providerPlaceholder": "Seleccionar proveedor",
|
||||
|
||||
@@ -86,6 +86,7 @@ export const settingsDict = {
|
||||
'settings.openchamber.tunnel.section.redeemedAccessLinks': '사용된 접속 링크',
|
||||
'settings.openchamber.tunnel.section.savedManagedRemoteTunnels': '저장된 Managed Remote 터널',
|
||||
'settings.openchamber.tunnel.notAvailable.cloudflaredNotFound': 'cloudflared를 찾을 수 없음',
|
||||
'settings.openchamber.tunnel.notAvailable.dependencyNotFound': '{dependency}를 찾을 수 없습니다.',
|
||||
'settings.openchamber.tunnel.notAvailable.installHint': '원격 터널 접속을 사용하려면 설치하세요:',
|
||||
'settings.openchamber.tunnel.field.provider': '프로바이더',
|
||||
'settings.openchamber.tunnel.field.providerPlaceholder': '프로바이더 선택',
|
||||
|
||||
@@ -814,6 +814,7 @@ export const settingsDict = {
|
||||
'settings.openchamber.tunnel.field.tunnelSessionTtl': 'TTL sesji tunelu',
|
||||
'settings.openchamber.tunnel.field.tunnelType': 'Typ tunelu',
|
||||
'settings.openchamber.tunnel.notAvailable.cloudflaredNotFound': 'Nie znaleziono cloudflared',
|
||||
'settings.openchamber.tunnel.notAvailable.dependencyNotFound': 'Nie znaleziono {dependency}.',
|
||||
'settings.openchamber.tunnel.notAvailable.installHint': 'Zainstaluj go, aby włączyć zdalny dostęp przez tunel:',
|
||||
'settings.openchamber.tunnel.note.cloudflareConnectorTarget': 'Docelowy adres konektora Cloudflare:',
|
||||
'settings.openchamber.tunnel.note.cloudflareConnectorTargetUse': 'Dla docelowego adresu konektora Cloudflare użyj',
|
||||
|
||||
@@ -86,6 +86,7 @@ export const settingsDict = {
|
||||
"settings.openchamber.tunnel.section.redeemedAccessLinks": "Links de acesso resgatados",
|
||||
"settings.openchamber.tunnel.section.savedManagedRemoteTunnels": "Túneis remotos gerenciados salvos",
|
||||
"settings.openchamber.tunnel.notAvailable.cloudflaredNotFound": "cloudflared não encontrado",
|
||||
"settings.openchamber.tunnel.notAvailable.dependencyNotFound": "{dependency} não foi encontrado.",
|
||||
"settings.openchamber.tunnel.notAvailable.installHint": "Instale para habilitar o acesso ao túnel remoto:",
|
||||
"settings.openchamber.tunnel.field.provider": "Provedor",
|
||||
"settings.openchamber.tunnel.field.providerPlaceholder": "Selecionar provedor",
|
||||
|
||||
@@ -86,6 +86,7 @@ export const settingsDict = {
|
||||
"settings.openchamber.tunnel.section.redeemedAccessLinks": "Використані посилання доступу",
|
||||
"settings.openchamber.tunnel.section.savedManagedRemoteTunnels": "Збережені керовані віддалені тунелі",
|
||||
"settings.openchamber.tunnel.notAvailable.cloudflaredNotFound": "cloudflared не знайдено",
|
||||
"settings.openchamber.tunnel.notAvailable.dependencyNotFound": "{dependency} не знайдено.",
|
||||
"settings.openchamber.tunnel.notAvailable.installHint": "Встановіть його, щоб увімкнути віддалений доступ через тунель:",
|
||||
"settings.openchamber.tunnel.field.provider": "Провайдер",
|
||||
"settings.openchamber.tunnel.field.providerPlaceholder": "Виберіть провайдера",
|
||||
|
||||
@@ -86,6 +86,7 @@ export const settingsDict = {
|
||||
'settings.openchamber.tunnel.section.redeemedAccessLinks': '已兑换访问链接',
|
||||
'settings.openchamber.tunnel.section.savedManagedRemoteTunnels': '已保存的 Managed Remote 隧道',
|
||||
'settings.openchamber.tunnel.notAvailable.cloudflaredNotFound': '未找到 cloudflared',
|
||||
'settings.openchamber.tunnel.notAvailable.dependencyNotFound': '未找到 {dependency}。',
|
||||
'settings.openchamber.tunnel.notAvailable.installHint': '安装它以启用远程隧道访问:',
|
||||
'settings.openchamber.tunnel.field.provider': '提供商',
|
||||
'settings.openchamber.tunnel.field.providerPlaceholder': '选择提供商',
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
'settings.openchamber.tunnel.section.redeemedAccessLinks': '已兌換存取連結',
|
||||
'settings.openchamber.tunnel.section.savedManagedRemoteTunnels': '已儲存的 managed remote tunnel',
|
||||
'settings.openchamber.tunnel.notAvailable.cloudflaredNotFound': '找不到 cloudflared',
|
||||
'settings.openchamber.tunnel.notAvailable.dependencyNotFound': '找不到 {dependency}。',
|
||||
'settings.openchamber.tunnel.notAvailable.installHint': '安裝它以啟用 remote tunnel 存取:',
|
||||
'settings.openchamber.tunnel.field.provider': '供應商',
|
||||
'settings.openchamber.tunnel.field.providerPlaceholder': '選擇供應商',
|
||||
|
||||
Reference in New Issue
Block a user