Skip to content

Commit 98f1e45

Browse files
author
Faxbot Agent
committed
feat(diagnostics): add HumbleFax diagnostics functionality
- Introduced a new endpoint `/admin/diagnostics/humblefax` to check HumbleFax connectivity, including DNS resolution, authentication presence, and webhook URL reachability. - Added a method `getHumbleFaxDiagnostics` in AdminAPIClient to fetch diagnostics data for the HumbleFax provider. - Enhanced the TunnelSettings component with a button to run HumbleFax diagnostics, displaying results in the UI. - Implemented local QR code generation for pairing codes, improving user experience during tunnel setup. These changes enhance the troubleshooting capabilities for the HumbleFax provider, allowing users to easily verify connectivity and configuration issues directly from the admin interface.
1 parent 04c681e commit 98f1e45

File tree

10 files changed

+488
-14
lines changed

10 files changed

+488
-14
lines changed

api/admin_ui/package-lock.json

Lines changed: 219 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/admin_ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
"@xterm/addon-web-links": "^0.11.0",
1919
"@xterm/xterm": "^5.5.0",
2020
"date-fns": "^4.1.0",
21+
"qrcode": "^1.5.4",
2122
"react": "^18.2.0",
2223
"react-dom": "^18.2.0",
2324
"react-hook-form": "^7.48.2",
2425
"react-router-dom": "^6.20.1"
2526
},
2627
"devDependencies": {
2728
"@testing-library/react": "^14.1.2",
29+
"@types/qrcode": "^1.5.5",
2830
"@types/react": "^18.2.43",
2931
"@types/react-dom": "^18.2.17",
3032
"@vitejs/plugin-react": "^4.2.0",

api/admin_ui/src/api/client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ export class AdminAPIClient {
109109
return res.json();
110110
}
111111

112+
async getHumbleFaxDiagnostics(): Promise<any> {
113+
const res = await this.fetch('/admin/diagnostics/humblefax');
114+
return res.json();
115+
}
116+
112117
// Hierarchical Configuration (v4)
113118
async getEffectiveConfig(): Promise<{ values: Record<string, any>; cache_stats?: any }> {
114119
const res = await this.fetch('/admin/config/v4/effective');

api/admin_ui/src/components/TunnelSettings.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ export default function TunnelSettings({ client, docsBase, hipaaMode, inboundBac
1717
const [testing, setTesting] = useState<boolean>(false);
1818
const [registering, setRegistering] = useState<boolean>(false);
1919
const [pairDialog, setPairDialog] = useState<{ open: boolean; code?: string; expires_at?: string }>({ open: false });
20+
const [pairQr, setPairQr] = useState<string | null>(null);
2021
const [logsLoading, setLogsLoading] = useState<boolean>(false);
2122
const [logs, setLogs] = useState<string[]>([]);
2223
const [isSinchActive, setIsSinchActive] = useState<boolean>(false);
2324
const [inboundEnabled, setInboundEnabled] = useState<boolean>(false);
2425
const [notice, setNotice] = useState<{ severity: 'success' | 'error' | 'info'; message: string } | null>(null);
2526
const [sinchDiag, setSinchDiag] = useState<any | null>(null);
2627
const [diagLoading, setDiagLoading] = useState<boolean>(false);
28+
const [hfDiag, setHfDiag] = useState<any | null>(null);
29+
const [hfDiagLoading, setHfDiagLoading] = useState<boolean>(false);
2730

2831
const [provider, setProvider] = useState<'none' | 'cloudflare' | 'wireguard' | 'tailscale'>('none');
2932
const [wg, setWg] = useState<{ endpoint?: string; server_key?: string; client_ip?: string; dns?: string }>({});
@@ -189,6 +192,12 @@ export default function TunnelSettings({ client, docsBase, hipaaMode, inboundBac
189192
try {
190193
const res = await client.createTunnelPairing();
191194
setPairDialog({ open: true, code: res.code, expires_at: res.expires_at });
195+
// Generate QR locally (no external network dependency)
196+
try {
197+
const { default: QRCode } = await import('qrcode');
198+
const dataUrl = await QRCode.toDataURL(String(res.code || ''), { width: 256, margin: 1 });
199+
setPairQr(dataUrl);
200+
} catch { setPairQr(null); }
192201
} catch (e: any) {
193202
setNotice({ severity: 'error', message: e?.message || 'Could not create pairing code' });
194203
}
@@ -235,6 +244,21 @@ export default function TunnelSettings({ client, docsBase, hipaaMode, inboundBac
235244
}
236245
};
237246

247+
const runHumbleFaxDiagnostics = async () => {
248+
setHfDiagLoading(true);
249+
setHfDiag(null);
250+
try {
251+
const res = await (client as any).getHumbleFaxDiagnostics?.();
252+
setHfDiag(res || {});
253+
const ok = Boolean(res?.dns_ok) && Boolean(res?.auth_present) && Boolean(res?.auth_ok) && Boolean(res?.webhook_url_ok);
254+
setNotice({ severity: ok ? 'success' : 'error', message: ok ? 'HumbleFax diagnostics: OK' : 'HumbleFax diagnostics found issues' });
255+
} catch (e: any) {
256+
setNotice({ severity: 'error', message: e?.message || 'Diagnostics failed' });
257+
} finally {
258+
setHfDiagLoading(false);
259+
}
260+
};
261+
238262
const cloudflareDisabled = Boolean(hipaaMode);
239263

240264
const fetchLogs = async () => {
@@ -483,6 +507,12 @@ export default function TunnelSettings({ client, docsBase, hipaaMode, inboundBac
483507
<InlineLoader loading={registering} />
484508
</Button>
485509
)}
510+
{(String(inboundBackend || '').toLowerCase() === 'humblefax') && (
511+
<Button variant="outlined" onClick={runHumbleFaxDiagnostics} disabled={hfDiagLoading} sx={{ borderRadius: 2 }}>
512+
Run HumbleFax Diagnostics
513+
<InlineLoader loading={hfDiagLoading} />
514+
</Button>
515+
)}
486516
{provider === 'cloudflare' && !cloudflareDisabled && (
487517
<Button variant="text" onClick={fetchLogs} disabled={logsLoading} sx={{ borderRadius: 2 }}>
488518
View Cloudflared Logs (tail)
@@ -505,6 +535,15 @@ export default function TunnelSettings({ client, docsBase, hipaaMode, inboundBac
505535
</Paper>
506536
)}
507537

538+
{hfDiag && (
539+
<Paper sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 2, mb: 2 }}>
540+
<Typography variant="subtitle2" sx={{ mb: 1 }}>HumbleFax Diagnostics</Typography>
541+
<Box component="pre" sx={{ m: 0, p: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: '0.8rem' }}>
542+
{JSON.stringify(hfDiag, null, 2)}
543+
</Box>
544+
</Paper>
545+
)}
546+
508547
{provider === 'cloudflare' && !cloudflareDisabled && logs.length > 0 && (
509548
<Paper sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 2, mb: 2 }}>
510549
<Typography variant="subtitle2" sx={{ mb: 1 }}>Cloudflared logs (last 50 lines)</Typography>
@@ -515,7 +554,7 @@ export default function TunnelSettings({ client, docsBase, hipaaMode, inboundBac
515554
)}
516555

517556
{/* Pairing dialog */}
518-
<Dialog open={pairDialog.open} onClose={() => setPairDialog({ open: false })}>
557+
<Dialog open={pairDialog.open} onClose={() => { setPairDialog({ open: false }); setPairQr(null); }}>
519558
<DialogTitle>iOS Pairing</DialogTitle>
520559
<DialogContent>
521560
<Typography variant="body2" sx={{ mb: 1 }}>
@@ -524,6 +563,11 @@ export default function TunnelSettings({ client, docsBase, hipaaMode, inboundBac
524563
<Typography variant="h4" sx={{ textAlign: 'center', letterSpacing: 4, my: 2 }}>
525564
{pairDialog.code}
526565
</Typography>
566+
{pairQr && (
567+
<Box sx={{ display: 'flex', justifyContent: 'center', my: 1 }}>
568+
<img src={pairQr} alt="Pairing QR" style={{ maxWidth: 256, width: '100%' }} />
569+
</Box>
570+
)}
527571
{pairDialog.expires_at && (
528572
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', textAlign: 'center' }}>
529573
Expires at {new Date(pairDialog.expires_at).toLocaleTimeString()}

api/app/main.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1999,6 +1999,136 @@ async def admin_diagnostics_sinch():
19991999
return SinchDiagnosticsOut(dns_ok=dns_ok, host=host, resolved=resolved, auth_present=auth_present, auth_ok=auth_ok, path_ok=path_ok, probes=probes)
20002000

20012001

2002+
# ===== Lightweight diagnostics (HumbleFax) =====
2003+
class HumbleFaxDiagnosticsOut(BaseModel):
2004+
backend: str = "humblefax"
2005+
dns_ok: bool
2006+
host: str
2007+
resolved: list[str] = []
2008+
auth_present: bool
2009+
auth_ok: bool
2010+
webhook_base: Optional[str] = None
2011+
webhook_url: Optional[str] = None
2012+
webhook_url_ok: bool = False
2013+
probes: dict[str, Any]
2014+
error: Optional[str] = None
2015+
2016+
2017+
@app.get("/admin/diagnostics/humblefax", response_model=HumbleFaxDiagnosticsOut, dependencies=[Depends(require_admin)])
2018+
async def admin_diagnostics_humblefax():
2019+
"""Check HumbleFax connectivity from inside the API container.
2020+
2021+
Non-destructive checks:
2022+
- DNS resolution for api.humblefax.com
2023+
- Basic auth presence and HTTP reachability
2024+
- Public webhook URL resolvable and reachable (HEAD/GET tolerance)
2025+
"""
2026+
import socket as _socket
2027+
from urllib.parse import urlparse as _urlparse
2028+
2029+
base_host = os.getenv("HUMBLEFAX_API_HOST", "api.humblefax.com").strip()
2030+
scheme = os.getenv("HUMBLEFAX_API_SCHEME", "https").strip()
2031+
base_url = f"{scheme}://{base_host}"
2032+
host = _urlparse(base_url).hostname or base_host
2033+
2034+
# DNS check
2035+
dns_ok = False
2036+
resolved: list[str] = []
2037+
try:
2038+
_, _, ips = _socket.gethostbyname_ex(host)
2039+
resolved = ips or []
2040+
dns_ok = len(resolved) > 0
2041+
except Exception:
2042+
dns_ok = False
2043+
2044+
# Auth presence
2045+
ak = (getattr(settings, 'humblefax_access_key', '') or os.getenv('HUMBLEFAX_ACCESS_KEY', '')).strip()
2046+
sk = (getattr(settings, 'humblefax_secret_key', '') or os.getenv('HUMBLEFAX_SECRET_KEY', '')).strip()
2047+
auth_present = bool(ak and sk)
2048+
2049+
probes: dict[str, Any] = {}
2050+
auth_ok = False
2051+
2052+
# Compute webhook base and URL similar to registration logic
2053+
try:
2054+
public_base = (
2055+
(_TUNNEL_STATE.get("public_url") if not _hipaa_posture_enabled() else None)
2056+
or (getattr(settings, 'humblefax_callback_base', '') or None)
2057+
or (settings.public_api_url or None)
2058+
)
2059+
webhook_base = (public_base or "").rstrip("/") or None
2060+
except Exception:
2061+
webhook_base = None
2062+
webhook_url = f"{webhook_base}/inbound/humblefax/webhook" if webhook_base else None
2063+
2064+
# Perform HTTP checks
2065+
try:
2066+
import httpx
2067+
# 1) HEAD unauthenticated to /webhook for general reachability
2068+
try:
2069+
with httpx.Client(timeout=8.0) as client:
2070+
r0 = client.head(f"{base_url}/webhook")
2071+
probes[f"{base_url}/webhook:HEAD"] = {"status": r0.status_code}
2072+
except Exception as e:
2073+
probes[f"{base_url}/webhook:HEAD"] = {"error": str(e)}
2074+
2075+
# 2) HEAD with basic auth to /webhook to validate credentials (status < 400 → good)
2076+
if auth_present:
2077+
try:
2078+
with httpx.Client(timeout=8.0) as client:
2079+
r1 = client.head(f"{base_url}/webhook", auth=(ak, sk))
2080+
probes[f"{base_url}/webhook:HEAD:auth"] = {"status": r1.status_code}
2081+
if r1.status_code < 400:
2082+
auth_ok = True
2083+
except Exception as e:
2084+
probes[f"{base_url}/webhook:HEAD:auth"] = {"error": str(e)}
2085+
2086+
# 3) Validate webhook target URL exists/reachable (tolerate 405 for method not allowed)
2087+
webhook_url_ok = False
2088+
if webhook_url:
2089+
try:
2090+
with httpx.Client(timeout=6.0, follow_redirects=True) as client:
2091+
r2 = client.head(webhook_url)
2092+
probes[f"{webhook_url}:HEAD"] = {"status": r2.status_code}
2093+
webhook_url_ok = (r2.status_code in (200, 201, 202, 204, 301, 302, 307, 308, 401, 403, 405))
2094+
if not webhook_url_ok:
2095+
# Fallback to GET to accommodate servers that do not support HEAD
2096+
r3 = client.get(webhook_url, headers={"Accept": "application/json"})
2097+
probes[f"{webhook_url}:GET"] = {"status": r3.status_code}
2098+
webhook_url_ok = (r3.status_code in (200, 201, 202, 204, 301, 302, 307, 308, 401, 403, 405))
2099+
except Exception as e:
2100+
probes[f"{webhook_url}:HEAD"] = {"error": str(e)}
2101+
webhook_url_ok = False
2102+
else:
2103+
webhook_url_ok = False
2104+
2105+
except Exception as e:
2106+
return HumbleFaxDiagnosticsOut(
2107+
dns_ok=dns_ok,
2108+
host=host,
2109+
resolved=resolved,
2110+
auth_present=auth_present,
2111+
auth_ok=False,
2112+
webhook_base=webhook_base,
2113+
webhook_url=webhook_url,
2114+
webhook_url_ok=False,
2115+
probes=probes,
2116+
error=str(e),
2117+
)
2118+
2119+
return HumbleFaxDiagnosticsOut(
2120+
dns_ok=dns_ok,
2121+
host=host,
2122+
resolved=resolved,
2123+
auth_present=auth_present,
2124+
auth_ok=auth_ok,
2125+
webhook_base=webhook_base,
2126+
webhook_url=webhook_url,
2127+
webhook_url_ok=bool(webhook_url) and webhook_url_ok,
2128+
probes=probes,
2129+
)
2130+
2131+
20022132
# ===== Mobile pairing (dev-friendly) =====
20032133
class PairIn(BaseModel):
20042134
code: str

docs/SINCH_SETUP.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,12 @@ Troubleshooting
7777
- 413: file too large → raise `MAX_FILE_SIZE_MB`.
7878
- 415: unsupported file type → only PDF/TXT.
7979
- Sinch API errors: verify Project ID, API key/secret, and region.
80+
81+
Diagnostics
82+
- Admin → Tools → Tunnels → Run Sinch Diagnostics
83+
- API: `GET /admin/diagnostics/sinch`
84+
- Checks DNS resolution, OAuth/basic auth reachability, and v3 path compatibility.
85+
86+
Implementation notes
87+
- The outbound adapter prefers the two‑step upload + send flow with multipart fallback.
88+
- The client automatically tries unversioned and `/v3` paths, and both unscoped and project‑scoped endpoints.

docs/diagnostics.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
layout: default
3+
title: Diagnostics
4+
nav_order: 45
5+
permalink: /diagnostics
6+
---
7+
8+
# Diagnostics
9+
10+
Faxbot includes lightweight, non-destructive diagnostics to validate provider connectivity and webhook reachability from inside the API container.
11+
12+
Endpoints (admin)
13+
- GET `/admin/diagnostics/sinch` → DNS, auth presence, OAuth/basic probe, v3 path compatibility; returns exact probed URLs + status codes.
14+
- GET `/admin/diagnostics/humblefax` → DNS, Basic auth reachability, computed webhook URL reachability (HEAD/GET tolerant), exact probe statuses.
15+
- POST `/admin/diagnostics/run` → Bounded, trait-driven checks across active providers (health, Ghostscript if required, storage when inbound requires it).
16+
17+
Admin Console
18+
- Tools → Tunnels page exposes “Run Sinch Diagnostics” and “Run HumbleFax Diagnostics” when relevant, and shows the raw JSON report.
19+
20+
Troubleshooting Tips
21+
- DNS failures: verify container DNS or override `SINCH_BASE_URL`/`HUMBLEFAX_API_HOST` if using a custom region/host.
22+
- Auth failures: confirm credentials in Settings and ensure the selected auth method (OAuth2 vs Basic) matches provider configuration.
23+
- Webhook URL not reachable: set `PUBLIC_API_URL` (HTTPS) or use a stable named tunnel. Quick tunnels can be rejected by providers.
24+
25+
Security
26+
- No PHI is logged. Probes avoid sending recipient numbers or document content.
27+
- Webhook reachability checks tolerate 405 Method Not Allowed to avoid accidental side effects.
28+

docs/humblefax.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Webhooks
5757
Admin UI
5858
- Use Setup Wizard (HumbleFax) to enter keys and webhook secret.
5959
- “Register HumbleFax Webhook” button registers callbacks to `${public_api_url}/inbound/humblefax/webhook`.
60+
- Run diagnostics: Tools → Tunnels → “Run HumbleFax Diagnostics” or `GET /admin/diagnostics/humblefax`.
6061

6162
Security
6263
- No PHI is logged (only job IDs and meta counters). Recipients and fromNumber are masked in debug logs.
@@ -66,6 +67,17 @@ One‑shot tunnel + webhook setup
6667
- scripts/setup-humblefax-tunnel.sh starts the API + Cloudflare sidecar, detects the public URL, sets HUMBLEFAX_CALLBACK_BASE, and registers the webhook.
6768
- Requirements: Docker running; API_KEY (defaults to fbk_live_local_admin if unset).
6869

70+
Diagnostics
71+
```
72+
curl -sS -H "X-API-Key: ${API_KEY:-fbk_live_local_admin}" \
73+
http://localhost:8080/admin/diagnostics/humblefax | jq .
74+
```
75+
Fields
76+
- `dns_ok`: provider host resolves from inside the container
77+
- `auth_present`/`auth_ok`: credentials configured and accepted by the endpoint
78+
- `webhook_url_ok`: your computed webhook URL is reachable (tolerant of 405)
79+
- If false, set `PUBLIC_API_URL` or `HUMBLEFAX_CALLBACK_BASE` to a stable HTTPS URL and retry.
80+
6981
Run
7082
```
7183
scripts/setup-humblefax-tunnel.sh

docs/mobile-pairing.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
layout: default
3+
title: Mobile Pairing (iOS)
4+
nav_order: 50
5+
permalink: /apps/ios/pairing
6+
---
7+
8+
# Mobile Pairing (iOS)
9+
10+
The iOS app pairs to your Faxbot server using a short numeric code. Pairing returns a scoped API key and base URLs so the app can reach your server.
11+
12+
Flow
13+
1) Admin Console → Tools → Tunnels → Generate iOS Pairing Code
14+
- Shows the numeric code and a QR (no secrets) that expires quickly.
15+
2) iOS app → Settings → Pairing
16+
- Enter the code or scan the QR. In dev (`ENABLE_LOCAL_ADMIN=true`), any code is accepted unless `MOBILE_PAIRING_CODE` is set.
17+
3) Server returns:
18+
- `base_urls`: `{ local, tunnel, public }` – the app chooses a remote base when available.
19+
- `token`: API key with minimal scopes: `inbound:list`, `inbound:read`, `fax:send`.
20+
21+
API
22+
- POST `/admin/tunnel/pair` (admin) → `{ code, expires_at }`
23+
- POST `/mobile/pair` (no admin) → `{ base_urls, token }` (validates code; dev bypass available)
24+
25+
Environment
26+
```
27+
ENABLE_LOCAL_ADMIN=true # dev only; bypasses code check unless MOBILE_PAIRING_CODE is set
28+
# Optional fixed code (dev labs / demos)
29+
MOBILE_PAIRING_CODE=123456
30+
```
31+
32+
Security Notes
33+
- Codes contain no secrets and expire rapidly.
34+
- The returned token is scoped and can be revoked from Admin → Keys.
35+
- In HIPAA mode, public/quick tunnel URLs are suppressed; prefer stable HTTPS (`PUBLIC_API_URL`) or HIPAA-capable VPN.
36+

docs/networking/TUNNELS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Options
2121

2222
Admin Console
2323
- Go to Settings → VPN Tunnel to configure provider and test connectivity.
24-
- Generate a short‑lived iOS pairing code (no secrets in QR or UI).
24+
- Generate a short‑lived iOS pairing code (no secrets in QR or UI). The Admin Console now renders the QR locally (no external QR service).
2525
- Terminal and Admin Actions remain local‑only; do not expose via tunnels.
2626

2727
Environment reference (examples)
@@ -48,4 +48,4 @@ Security notes
4848
Troubleshooting
4949
- Use Tools → Scripts & Tests to tail cloudflared logs (dev only) and run reachability checks.
5050
- Ensure Ghostscript is installed; see readiness and diagnostics pages if tests fail.
51-
51+
- For HumbleFax, unstable quick tunnels can be rejected during webhook validation. Prefer a stable HTTPS `PUBLIC_API_URL` or a named tunnel.

0 commit comments

Comments
 (0)