1gbits / hostdoctor
Health checks for hosts, servers and web services - HTTP, TLS, TCP, DNS, security headers and performance timing with a normalized report and a health score.
Requires
- php: >=8.1
- ext-curl: *
- ext-openssl: *
- composer/ca-bundle: ^1.4
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0 || ^12.0
This package is not auto-updated.
Last update: 2026-08-11 11:55:20 UTC
README
Diagnose your server in one command.
HostDoctor is an SDK for checking the health of a host, server, URL or network service — HTTP, TLS, TCP, DNS, security headers and performance timing — condensed into one normalized report with a health score.
$ hostdoctor check https://example.com
HostDoctor
Target https://example.com
Time 2026-08-10T12:00:00Z (684 ms)
Network ✓ IPv4 203.0.113.10 | IPv6 available
TCP :443 ✓ open (34 ms)
TLS ✓ TLSv1.3 | valid | 71 days remaining | Let's Encrypt
HTTP ✓ 200 | HTTP/2 | 182 ms | 1 redirect
Security ⚠ 4/6 headers present (score 70) | missing: content-security-policy
Timing DNS 12 ms | TCP 34 ms | TLS 51 ms | TTFB 106 ms | Total 181 ms
Issues
⚠ SECURITY_HEADER_MISSING Content-Security-Policy header is missing.
Score: 91/100
Status: Healthy
HTTP • TLS • TCP • Network • Performance • Security
Installation
composer require 1gbits/hostdoctor
Requires PHP 8.1+, ext-curl and ext-openssl.
Quick start
use OneGBits\HostDoctor\HostDoctor; $doctor = new HostDoctor(); $report = $doctor->check('https://example.com'); $report->score; // 91 $report->status->value; // "healthy" | "warning" | "degraded" | "critical" echo $report->toJson(); // normalized JSON report
Every method accepts a URL, hostname, IP, or host:port where it makes sense, and every method returns the same normalized Report.
$doctor->check('https://example.com'); // full pipeline $doctor->check('192.0.2.10:22'); // network + TCP port $doctor->check('example.com'); // network + port scan + HTTP/TLS if 80/443 open $doctor->http('https://api.example.com/health', ['expected_status' => 200]); $doctor->tls('example.com'); // certificate validity, expiry, hostname match $doctor->port('192.0.2.10', 22); // single TCP port (failure = issue) $doctor->ports('192.0.2.10', [22, 80, 443, 3306]); // parallel scan (facts, not failures) $doctor->network('example.com'); // IPv4/IPv6 resolution $doctor->redirects('http://example.com');// full redirect chain, loop detection $doctor->security('https://example.com');// security response headers
Reading the report
$report = $doctor->check('https://example.com'); foreach ($report->issues as $issue) { // Codes are stable and machine-readable; messages are for humans. if ($issue->code === 'TLS_EXPIRING_SOON') { alertOps($issue->message); } } $report->check('tls')?->data['certificate']['days_remaining']; $report->hasIssue('REDIRECT_LOOP');
The JSON structure is defined in spec/report.schema.json and is shared by every HostDoctor implementation:
{
"version": "1.0",
"target": { "input": "https://example.com", "type": "url", "host": "example.com", "scheme": "https", "port": null },
"status": "healthy",
"score": 91,
"started_at": "2026-08-10T12:00:00Z",
"duration_ms": 684,
"checks": {
"network": { "ipv4": ["203.0.113.10"], "ipv6": ["2001:db8::10"], "ipv4_available": true, "ipv6_available": true },
"tcp": { "ports": { "443": { "status": "open", "connect_time_ms": 34.2 } } },
"tls": { "enabled": true, "valid": true, "protocol": "TLSv1.3", "certificate": { "issuer": "Let's Encrypt", "days_remaining": 71 } },
"http": { "status_code": 200, "protocol": "HTTP/2", "response_time_ms": 182.4, "redirects": { "count": 1, "loop": false } },
"security": { "score": 70, "headers": { "content_security_policy": { "present": false } } },
"performance": { "dns_ms": 12.1, "tcp_connect_ms": 34.2, "tls_handshake_ms": 51.0, "ttfb_ms": 106.3, "total_ms": 181.9 }
},
"issues": [
{ "code": "SECURITY_HEADER_MISSING", "severity": "warning", "message": "Content-Security-Policy header is missing.", "check": "security" }
],
"metadata": { "engine": "hostdoctor-php", "engine_version": "1.0.0", "safe_mode": true }
}
CLI
hostdoctor check https://example.com hostdoctor check https://example.com --json hostdoctor tls example.com hostdoctor port 203.0.113.10 22 hostdoctor ports 203.0.113.10 22,80,443,3306 hostdoctor redirects http://example.com hostdoctor security https://example.com hostdoctor network example.com
Exit codes are CI-friendly:
| Code | Meaning |
|---|---|
| 0 | Healthy |
| 1 | Warning |
| 2 | Degraded / Critical |
| 3 | Internal error / invalid usage |
# e.g. verify a deployment in GitHub Actions - name: Post-deploy health check run: vendor/bin/hostdoctor check https://api.example.com --expect=200 --timeout=10
Options
Pass options to the constructor (defaults for every call) or per call:
$doctor = new HostDoctor([ 'timeout' => 5.0, // request timeout, seconds 'connect_timeout' => 5.0, // TCP connect timeout, seconds 'retries' => 2, // retries after timeouts only — a TLS failure is never retried 'safe_mode' => true, // SSRF guard (see below) 'max_redirects' => 10, 'max_body_bytes' => 1048576, // response bodies are cut off after 1 MB 'tls_expiry_warning_days' => 14, // TLS_EXPIRING_SOON threshold 'slow_response_ms' => 2000, // HTTP_TOO_SLOW threshold 'expected_status' => null, // int or int[]; default: any status < 400 'headers' => [], // extra request headers (redacted in reports) 'method' => 'GET', // or HEAD 'verify_tls' => true, 'user_agent' => null, // default: HostDoctor/<version> 'scoring' => [], // e.g. ['tls' => 30, 'performance' => 10] 'ports' => [80, 443], // ports scanned for bare-host checks 'ca_file' => null, // custom CA bundle ]); $doctor->http($url, ['expected_status' => [200, 204], 'timeout' => 10.0]);
Health score
The score starts at 100. Every issue deducts points in its category, and each category's total deduction is capped at its weight:
| Category | Weight | Example deductions |
|---|---|---|
| Connectivity | 25 | DNS failure −25, port refused −25 |
| HTTP | 20 | unreachable −20, unexpected status −20 |
| TLS | 20 | invalid/expired/mismatch −20, expiring soon −5 |
| Performance | 15 | slow response −10 |
| Security headers | 10 | CSP/HSTS −3 each, others −1 |
| IPv6 | 5 | no AAAA record −2 |
| Redirects | 5 | loop −5, too many −5 |
| Score | Status |
|---|---|
| 90–100 | healthy |
| 75–89 | warning |
| 50–74 | degraded |
| 0–49 | critical |
Weights are tunable — deductions scale with the weight:
new HostDoctor(['scoring' => ['tls' => 30, 'performance' => 10]]);
Issue codes
Issues carry stable machine-readable codes (full catalog in spec/issues.json):
DNS_RESOLUTION_FAILED · CONNECTION_TIMEOUT · CONNECTION_REFUSED · HTTP_UNREACHABLE · HTTP_UNEXPECTED_STATUS · HTTP_TOO_SLOW · REDIRECT_LOOP · TOO_MANY_REDIRECTS · REDIRECT_BLOCKED · TLS_UNAVAILABLE · TLS_INVALID · TLS_EXPIRED · TLS_EXPIRING_SOON · TLS_HOSTNAME_MISMATCH · IPV6_UNAVAILABLE · SECURITY_HEADER_MISSING
Safe mode (SSRF protection)
HostDoctor opens connections to caller-supplied targets. If those targets come from users (a "check my site" form, a public API), an attacker could point it at http://127.0.0.1 or http://169.254.169.254 to probe your internal network.
Safe mode is on by default and refuses to touch loopback, link-local, private/CGNAT, multicast and other special-purpose ranges — including targets that only become internal after DNS resolution or an HTTP redirect. In safe mode the vetted IP is pinned for the actual connection, so DNS rebinding between check and connect doesn't work either. Blocked targets throw a TargetBlockedException.
Checking your own infrastructure? Turn it off explicitly:
$doctor = new HostDoctor(['safe_mode' => false]);
hostdoctor port 10.0.0.5 22 --no-safe-mode
Two related guardrails are always on:
- Response bodies are cut off after
max_body_bytes(default 1 MB) — a health check never downloads a 10 GB file. Authorization,Cookieand similar headers never appear in reports; they are replaced with[REDACTED].
What HostDoctor is (and isn't)
HostDoctor is a library, not a monitoring service: your application calls it, it runs the checks right now and hands back a structured result. No polling, no history, no alerting — those are things you can build with it.
The security check inspects observable configuration (response headers). A perfect security score means the headers are set — it is not a vulnerability scan and never a claim that a site is "secure".
Testing
composer install
composer test
Unit tests are network-free. The network PHPUnit group is reserved for integration tests and excluded by default.
Roadmap
- 1.1 — batch checks, JSON body expectations for API endpoints, config file, CI mode, retry policies
- 1.2 — probes for WebSocket, SSH banner, SMTP, MySQL, PostgreSQL, Redis
- 2.0 — public plugin system (
$doctor->use(new RedisProbe()))
The report schema, issue codes and scoring model live in spec/ and are the contract for upcoming implementations in other languages.
License
MIT