BangSo/apps/mobile/lib/endpoint.ts

101 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const LOCAL_API = "http://127.0.0.1:3100";
const DEFAULT_API = process.env.EXPO_PUBLIC_API_URL ?? LOCAL_API;
export type EndpointResult = { ok: true; url: string } | { ok: false; error: string };
export function defaultApiBase() {
return originOnly(DEFAULT_API) ?? LOCAL_API;
}
export function normalizeApiBase(input: string): EndpointResult {
const trimmed = input.trim();
if (!trimmed) return { ok: false, error: "Enter a server URL" };
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
let parsed: URL;
try {
parsed = new URL(withScheme);
} catch {
return { ok: false, error: "That doesnt look like a URL" };
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return { ok: false, error: "Use an http or https URL" };
}
if (!parsed.hostname) return { ok: false, error: "That URL is missing a host" };
if (parsed.protocol === "http:" && !isLanOrLocalHost(parsed.hostname)) {
return { ok: false, error: "Public servers need https://" };
}
const url = `${parsed.protocol}//${parsed.host}`;
return { ok: true, url };
}
export function displayApiHost(url: string) {
try {
const parsed = new URL(url);
return parsed.host;
} catch {
return url;
}
}
export function usesCustomApiBase(url: string, fallback = defaultApiBase()) {
return url !== fallback;
}
export function apiBaseWarning(url: string): string | null {
try {
const parsed = new URL(url);
if (parsed.protocol === "http:" && !isLanOrLocalHost(parsed.hostname)) {
return "Public servers need https://. HTTP only works on your local network.";
}
} catch {
return null;
}
return null;
}
export async function probeApiBase(
input: string,
fetchImpl: typeof fetch = fetch,
): Promise<EndpointResult> {
const parsed = normalizeApiBase(input);
if (!parsed.ok) return parsed;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 8_000);
try {
const res = await fetchImpl(`${parsed.url}/rpc/health`, {
method: "POST",
headers: { "content-type": "application/json", origin: "rakazo://" },
body: JSON.stringify({ json: {} }),
signal: controller.signal,
});
const body = (await res.json().catch(() => ({}))) as {
json?: { ok?: boolean };
error?: { message?: string };
};
if (!res.ok || body.error || body.json?.ok !== true) {
return { ok: false, error: "That URL did not look like a BangSo Bot server" };
}
return parsed;
} catch {
return { ok: false, error: "Could not reach that server" };
} finally {
clearTimeout(timer);
}
}
function originOnly(value: string) {
const parsed = normalizeApiBase(value);
return parsed.ok ? parsed.url : null;
}
function isLanOrLocalHost(hostname: string) {
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
if (host === "localhost" || host === "127.0.0.1" || host === "::1") return true;
if (host.endsWith(".local")) return true;
if (/^10(?:\.\d{1,3}){3}$/.test(host)) return true;
if (/^192\.168(?:\.\d{1,3}){2}$/.test(host)) return true;
if (/^172\.(1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2}$/.test(host)) return true;
if (/^100\.(6[4-9]|[7-9]\d|1[0-1]\d|12[0-7])(?:\.\d{1,3}){2}$/.test(host)) return true;
return false;
}