mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(bootcheck): warn instead of block on public HTTP core URLs (#3031)
This commit is contained in:
@@ -38,6 +38,29 @@ import LanguageSelect from '../LanguageSelect';
|
||||
const log = debug('boot-check');
|
||||
const logError = debug('boot-check:error');
|
||||
|
||||
/**
|
||||
* Plain HTTP to a public host is insecure (unencrypted traffic), but we no
|
||||
* longer block it — return a non-blocking warning string so the UI can nudge
|
||||
* the user toward HTTPS while still letting them proceed. Returns null when the
|
||||
* URL is empty, unparseable, HTTPS, or points at a local/private host.
|
||||
*/
|
||||
function httpPublicHostWarning(
|
||||
rawUrl: string,
|
||||
t: (key: string, fallback?: string) => string
|
||||
): string | null {
|
||||
const trimmed = rawUrl.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
const parsed = new URL(normalizeRpcUrl(trimmed));
|
||||
if (parsed.protocol === 'http:' && !isLocalOrPrivateNetworkHost(parsed.hostname)) {
|
||||
return t('bootCheck.httpPublicWarning');
|
||||
}
|
||||
} catch {
|
||||
// Unparseable URL — the error path in validateInputs handles messaging.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal types
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -107,6 +130,7 @@ function ModePicker({ onConfirm }: PickerProps) {
|
||||
const [cloudUrl, setCloudUrl] = useState('');
|
||||
const [cloudToken, setCloudToken] = useState('');
|
||||
const [urlError, setUrlError] = useState<string | null>(null);
|
||||
const [urlWarning, setUrlWarning] = useState<string | null>(null);
|
||||
const [tokenError, setTokenError] = useState<string | null>(null);
|
||||
const [testStatus, setTestStatus] = useState<TestStatus>({ kind: 'idle' });
|
||||
|
||||
@@ -135,12 +159,6 @@ function ModePicker({ onConfirm }: PickerProps) {
|
||||
setUrlError(t('bootCheck.urlMustStartWith'));
|
||||
return null;
|
||||
}
|
||||
if (parsed.protocol === 'http:' && !isLocalOrPrivateNetworkHost(parsed.hostname)) {
|
||||
setUrlError(
|
||||
'HTTP core URLs are only allowed for localhost or private network hosts. Use HTTPS for public hosts.'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
setUrlError(t('bootCheck.validUrlRequired'));
|
||||
return null;
|
||||
@@ -287,13 +305,18 @@ function ModePicker({ onConfirm }: PickerProps) {
|
||||
placeholder={t('bootCheck.rpcUrlPlaceholder')}
|
||||
value={cloudUrl}
|
||||
onChange={e => {
|
||||
setCloudUrl(e.target.value);
|
||||
const next = e.target.value;
|
||||
setCloudUrl(next);
|
||||
setUrlError(null);
|
||||
setUrlWarning(httpPublicHostWarning(next, t));
|
||||
setTestStatus({ kind: 'idle' });
|
||||
}}
|
||||
className="rounded-lg border border-stone-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-sm text-stone-900 dark:text-neutral-100 placeholder-stone-400 dark:placeholder-neutral-500 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
/>
|
||||
{urlError && <p className="text-xs text-red-600">{urlError}</p>}
|
||||
{!urlError && urlWarning && (
|
||||
<p className="text-xs text-amber-600 dark:text-amber-500">{urlWarning}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-stone-700 dark:text-neutral-200">
|
||||
|
||||
@@ -222,15 +222,34 @@ describe('BootCheckGate — picker (unset mode)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects public HTTP cloud URLs', () => {
|
||||
it('warns about public HTTP cloud URLs but does not block them', async () => {
|
||||
mockRunBootCheck.mockResolvedValue({ kind: 'match' });
|
||||
|
||||
renderGate();
|
||||
|
||||
fireEvent.click(screen.getByText('Run on the Cloud (Complex)'));
|
||||
const urlInput = screen.getByPlaceholderText(/https:\/\/core\.example\.com/);
|
||||
fireEvent.change(urlInput, { target: { value: 'http://core.example.com/rpc' } });
|
||||
|
||||
// Non-blocking warning shows inline as soon as the public HTTP URL is typed.
|
||||
expect(screen.getByText(/traffic will not be encrypted/i)).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(/Bearer token/i), {
|
||||
target: { value: 'tok-1234' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue' }));
|
||||
|
||||
expect(screen.getByText(/HTTP core URLs are only allowed/)).toBeInTheDocument();
|
||||
// The boot check still proceeds with the HTTP URL.
|
||||
await waitFor(() => {
|
||||
expect(mockRunBootCheck).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: 'cloud',
|
||||
url: 'http://core.example.com/rpc',
|
||||
token: 'tok-1234',
|
||||
}),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('clears the token error as soon as the user types into the token field', () => {
|
||||
|
||||
@@ -1995,6 +1995,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.urlMustStartWith': 'يجب أن يبدأ عنوان URL بـ http:// أو https://',
|
||||
'bootCheck.validUrlRequired': 'لا يبدو أن هذا عنوان URL صالح (جرّب https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': 'سنحتاج إلى رمز مصادقة للاتصال.',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'هذا عنوان HTTP عادي على مضيف عام — لن تكون حركة البيانات مشفّرة. استخدم HTTPS ما لم تكن تثق بهذه الشبكة.',
|
||||
'bootCheck.chooseCoreMode': 'اختر بيئة التشغيل',
|
||||
'bootCheck.connectToCore': 'اتصل ببيئة تشغيلك',
|
||||
'bootCheck.desktopDescription': 'يحتاج OpenHuman إلى بيئة تشغيل للعمل. اختر مكانها.',
|
||||
|
||||
@@ -2035,6 +2035,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.validUrlRequired':
|
||||
'এটি বৈধ URL মনে হচ্ছে না (চেষ্টা করুন https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': 'সংযোগ করতে একটি অথ টোকেন প্রয়োজন।',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'এটি একটি পাবলিক হোস্টে সাধারণ HTTP URL — ট্রাফিক এনক্রিপ্ট করা হবে না। এই নেটওয়ার্কে আস্থা না থাকলে HTTPS ব্যবহার করুন।',
|
||||
'bootCheck.chooseCoreMode': 'একটি রানটাইম বেছে নিন',
|
||||
'bootCheck.connectToCore': 'আপনার রানটাইমে সংযুক্ত হন',
|
||||
'bootCheck.desktopDescription':
|
||||
|
||||
@@ -2086,6 +2086,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.validUrlRequired':
|
||||
'Das sieht nicht nach einem gültigen URL aus (versuche es mit https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': 'Für die Verbindung benötigen wir ein Authentifizierungstoken.',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'Dies ist eine reine HTTP-URL auf einem öffentlichen Host — der Datenverkehr wird nicht verschlüsselt. Verwende HTTPS, sofern du diesem Netzwerk nicht vertraust.',
|
||||
'bootCheck.chooseCoreMode': 'Wähle eine Laufzeit aus',
|
||||
'bootCheck.connectToCore': 'Stelle eine Verbindung zu deiner Laufzeit her',
|
||||
'bootCheck.desktopDescription':
|
||||
|
||||
@@ -2253,6 +2253,8 @@ const en: TranslationMap = {
|
||||
'bootCheck.validUrlRequired':
|
||||
"That doesn't look like a valid URL (try https://core.example.com/rpc)",
|
||||
'bootCheck.tokenRequired': "We'll need an auth token to connect.",
|
||||
'bootCheck.httpPublicWarning':
|
||||
'This is a plain HTTP URL on a public host — traffic will not be encrypted. Use HTTPS unless you trust this network.',
|
||||
'bootCheck.chooseCoreMode': 'Select a Runtime',
|
||||
'bootCheck.connectToCore': 'Connect to Your Runtime',
|
||||
'bootCheck.desktopDescription': 'OpenHuman needs a runtime to think. Pick where it should live.',
|
||||
|
||||
@@ -2078,6 +2078,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.validUrlRequired':
|
||||
'Eso no parece una URL válida (prueba con https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': 'Necesitaremos un token de autenticación para conectarnos.',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'Esta es una URL HTTP sin cifrar en un host público: el tráfico no estará cifrado. Usa HTTPS a menos que confíes en esta red.',
|
||||
'bootCheck.chooseCoreMode': 'Seleccionar un runtime',
|
||||
'bootCheck.connectToCore': 'Conectar a tu runtime',
|
||||
'bootCheck.desktopDescription':
|
||||
|
||||
@@ -2084,6 +2084,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.validUrlRequired':
|
||||
'Ça ne ressemble pas à une URL valide (essaie https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': "On aura besoin d'un token d'authentification pour se connecter.",
|
||||
'bootCheck.httpPublicWarning':
|
||||
'Ceci est une URL HTTP en clair sur un hôte public — le trafic ne sera pas chiffré. Utilisez HTTPS sauf si vous faites confiance à ce réseau.',
|
||||
'bootCheck.chooseCoreMode': 'Sélectionner un runtime',
|
||||
'bootCheck.connectToCore': 'Connecte-toi à ton runtime',
|
||||
'bootCheck.desktopDescription':
|
||||
|
||||
@@ -2034,6 +2034,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.urlMustStartWith': 'URL को http:// या https:// से शुरू होना चाहिए',
|
||||
'bootCheck.validUrlRequired': 'यह सही URL नहीं लगता (कोशिश करें https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': 'कनेक्ट करने के लिए एक auth टोकन चाहिए।',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'यह किसी सार्वजनिक होस्ट पर सादा HTTP URL है — ट्रैफ़िक एन्क्रिप्ट नहीं होगा। जब तक आपको इस नेटवर्क पर भरोसा न हो, HTTPS का उपयोग करें।',
|
||||
'bootCheck.chooseCoreMode': 'रनटाइम चुनें',
|
||||
'bootCheck.connectToCore': 'अपने रनटाइम से कनेक्ट करें',
|
||||
'bootCheck.desktopDescription':
|
||||
|
||||
@@ -2037,6 +2037,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.urlMustStartWith': 'URL harus diawali dengan http:// atau https://',
|
||||
'bootCheck.validUrlRequired': 'Itu bukan URL yang valid (coba https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': 'Kami memerlukan token autentikasi untuk terhubung.',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'Ini adalah URL HTTP biasa pada host publik — lalu lintas tidak akan dienkripsi. Gunakan HTTPS kecuali Anda memercayai jaringan ini.',
|
||||
'bootCheck.chooseCoreMode': 'Pilih Runtime',
|
||||
'bootCheck.connectToCore': 'Hubungkan ke Runtime Anda',
|
||||
'bootCheck.desktopDescription':
|
||||
|
||||
@@ -2068,6 +2068,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.urlMustStartWith': "L'URL deve iniziare con http:// o https://",
|
||||
'bootCheck.validUrlRequired': 'Non sembra un URL valido (prova https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': 'Servirà un token di autenticazione per connettersi.',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'Questo è un URL HTTP in chiaro su un host pubblico — il traffico non sarà cifrato. Usa HTTPS a meno che tu non ti fidi di questa rete.',
|
||||
'bootCheck.chooseCoreMode': 'Seleziona un runtime',
|
||||
'bootCheck.connectToCore': 'Connetti al tuo runtime',
|
||||
'bootCheck.desktopDescription':
|
||||
|
||||
@@ -2014,6 +2014,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.urlMustStartWith': 'URL은 http:// 또는 https://로 시작해야 합니다',
|
||||
'bootCheck.validUrlRequired': '유효한 URL처럼 보이지 않습니다(예: https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': '연결하려면 인증 토큰이 필요합니다.',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'공개 호스트의 일반 HTTP URL입니다 — 트래픽이 암호화되지 않습니다. 이 네트워크를 신뢰하지 않는다면 HTTPS를 사용하세요.',
|
||||
'bootCheck.chooseCoreMode': '런타임 선택',
|
||||
'bootCheck.connectToCore': '런타임에 연결',
|
||||
'bootCheck.desktopDescription':
|
||||
|
||||
@@ -2058,6 +2058,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.validUrlRequired':
|
||||
'To nie wygląda na prawidłowy adres URL (spróbuj https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': 'Aby się połączyć, potrzebujemy tokenu uwierzytelniania.',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'To zwykły adres HTTP na publicznym hoście — ruch nie będzie szyfrowany. Użyj HTTPS, chyba że ufasz tej sieci.',
|
||||
'bootCheck.chooseCoreMode': 'Wybierz środowisko',
|
||||
'bootCheck.connectToCore': 'Połącz się ze swoim środowiskiem',
|
||||
'bootCheck.desktopDescription':
|
||||
|
||||
@@ -2075,6 +2075,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.validUrlRequired':
|
||||
'Isso não parece uma URL válida (tente https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': 'Vamos precisar de um token de autenticação para conectar.',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'Este é um URL HTTP comum em um host público — o tráfego não será criptografado. Use HTTPS a menos que você confie nesta rede.',
|
||||
'bootCheck.chooseCoreMode': 'Selecionar um Runtime',
|
||||
'bootCheck.connectToCore': 'Conectar ao Seu Runtime',
|
||||
'bootCheck.desktopDescription':
|
||||
|
||||
@@ -2049,6 +2049,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.validUrlRequired':
|
||||
'Похоже, это не корректный URL (попробуй https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': 'Для подключения нужен токен авторизации.',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'Это обычный HTTP-адрес на публичном хосте — трафик не будет зашифрован. Используйте HTTPS, если вы не доверяете этой сети.',
|
||||
'bootCheck.chooseCoreMode': 'Выбрать среду выполнения',
|
||||
'bootCheck.connectToCore': 'Подключиться к среде выполнения',
|
||||
'bootCheck.desktopDescription':
|
||||
|
||||
@@ -1936,6 +1936,8 @@ const messages: TranslationMap = {
|
||||
'bootCheck.urlMustStartWith': 'URL 必须以 http:// 或 https:// 开头',
|
||||
'bootCheck.validUrlRequired': '请输入有效的 URL(例如 https://core.example.com/rpc)',
|
||||
'bootCheck.tokenRequired': '请输入核心认证令牌。',
|
||||
'bootCheck.httpPublicWarning':
|
||||
'这是公网主机上的纯 HTTP 地址——流量不会被加密。除非你信任该网络,否则请使用 HTTPS。',
|
||||
'bootCheck.chooseCoreMode': '选择核心模式',
|
||||
'bootCheck.connectToCore': '连接到你的核心',
|
||||
'bootCheck.desktopDescription': 'OpenHuman 需要一个运行中的核心才能工作。请选择连接方式。',
|
||||
|
||||
Reference in New Issue
Block a user