fix(bootcheck): warn instead of block on public HTTP core URLs (#3031)

This commit is contained in:
Steven Enamakel
2026-05-30 16:32:26 -07:00
committed by GitHub
parent 5345f2a834
commit e092efdf88
16 changed files with 79 additions and 9 deletions
@@ -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', () => {
+2
View File
@@ -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 إلى بيئة تشغيل للعمل. اختر مكانها.',
+2
View File
@@ -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':
+2
View File
@@ -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':
+2
View File
@@ -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.',
+2
View File
@@ -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':
+2
View File
@@ -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':
+2
View File
@@ -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':
+2
View File
@@ -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':
+2
View File
@@ -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':
+2
View File
@@ -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':
+2
View File
@@ -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':
+2
View File
@@ -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':
+2
View File
@@ -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':
+2
View File
@@ -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 需要一个运行中的核心才能工作。请选择连接方式。',