1gbits / hostdoctor-laravel
Health checks for hosts, servers and web services in Laravel. HTTP, TLS, TCP, DNS, security headers and a normalized health score.
Requires
- php: ^8.0
- ext-curl: *
- ext-json: *
- ext-openssl: *
- illuminate/console: ^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/support: ^8.0|^9.0|^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^6.23|^7.0|^8.0|^9.0|^10.0
- phpunit/phpunit: ^9.5.10|^10.5|^11.5
This package is not auto-updated.
Last update: 2026-08-12 05:26:59 UTC
README
Diagnose hosts, servers and web services from your Laravel app — in one call.
HTTP • TLS • TCP • DNS • Security headers • Health score
$ php artisan hostdoctor:check https://example.com
HostDoctor v1.0.0
Target: https://example.com
Network OK IPv4 203.0.113.10 · IPv6 available
Ports OK 80 open (12 ms) · 443 open (11 ms)
TLS OK TLSv1.3 · valid · expires in 77 day(s)
HTTP OK 200 · HTTP/2 · 184 ms · 0 redirect(s)
Security ?? missing: Content-Security-Policy
Issues (1):
warning SECURITY_HEADER_MISSING Content-Security-Policy header is missing
Score: 97/100
Status: HEALTHY
HostDoctor is an SDK, not a monitoring SaaS: your application calls it right now, and gets back a normalized, machine-readable report — the same schema this package will share with future HostDoctor SDKs in other languages.
Requirements
- PHP 8.0+ with
curl,opensslandjsonextensions - Laravel 8, 9, 10, 11 or 12
Installation
composer require 1gbits/hostdoctor-laravel
The service provider and the HostDoctor facade are auto-discovered. To customize configuration:
php artisan vendor:publish --tag=hostdoctor-config
Quick start
use OneGbits\HostDoctor\Facades\HostDoctor; $report = HostDoctor::check('https://example.com'); $report->status; // "healthy" | "warning" | "degraded" | "critical" $report->score; // 0–100 $report->issues; // Issue[] with stable machine-readable codes $report->checks; // normalized per-check data $report->toArray(); // full schema-shaped array $report->toJson(); // same as JSON if ($report->hasIssue('TLS_EXPIRING_SOON')) { // alert someone }
Targets can be a URL, hostname, IP, or host:port:
HostDoctor::check('https://api.example.com/health'); HostDoctor::check('example.com'); HostDoctor::check('203.0.113.10'); // network + ports only HostDoctor::check('203.0.113.10:22'); // e.g. "is SSH up after provisioning?"
Options
HostDoctor::check('https://api.example.com/health', [ 'timeout' => 3.0, 'expected_status' => [200, 204], 'ports' => [80, 443, 8080], 'headers' => ['Authorization' => 'Bearer ...'], // redacted in the report ]);
The individual doctors
Every doctor validates the target against safe mode first, then returns a plain array.
HTTP
$http = HostDoctor::http('https://example.com'); // status_code, response_time_ms, protocol ("HTTP/2"), final_url, // redirects, redirect_chain, redirect_loop, headers, timing // (dns_ms / tcp_connect_ms / tls_handshake_ms / ttfb_ms / total_ms)
Redirects
HostDoctor::redirects('http://example.com'); // ['count' => 2, 'loop' => false, 'chain' => [['url' => ..., 'status' => 301], ...]]
TLS
$tls = HostDoctor::tls('example.com'); // port 443 $tls = HostDoctor::tls('example.com', 8443); // enabled, valid, hostname_match, protocol ("TLSv1.3"), // certificate: subject, issuer, valid_from, valid_until, days_remaining, san // failure: "expired" | "hostname_mismatch" | "untrusted" | "unreachable" | null
TCP ports
HostDoctor::port('203.0.113.10', 22); // ['port' => 22, 'status' => 'open', 'latency_ms' => 37, 'error' => null] HostDoctor::ports('203.0.113.10', [22, 80, 443, 3306]); // ['22' => [...], '80' => [...], ...]
Network (DNS / dual stack)
HostDoctor::network('example.com'); // ['reachable' => true, 'ipv4' => [...], 'ipv6' => [...], // 'ipv4_available' => true, 'ipv6_available' => true]
Security headers
HostDoctor::security('https://example.com'); // per-header presence of Strict-Transport-Security, Content-Security-Policy, // X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy
HostDoctor reports observable configuration — it never claims a site is "secure", and it is not a vulnerability scanner.
Artisan commands
php artisan hostdoctor:check https://example.com php artisan hostdoctor:check https://api.example.com --expect=200 --timeout=3 php artisan hostdoctor:check https://example.com --json php artisan hostdoctor:tls example.com --port=443 php artisan hostdoctor:port 203.0.113.10 22 80 443
Exit codes (CI-friendly)
| Code | Meaning |
|---|---|
0 |
healthy |
1 |
warning |
2 |
degraded / critical (for tls/port: check failed) |
3 |
internal error (invalid target, blocked by safe mode, ...) |
Use it as a deploy gate:
# GitHub Actions - name: Verify deployment run: php artisan hostdoctor:check https://api.example.com --expect=200
The report schema
Every full check returns the same shape (schema version 1.0):
{
"version": "1.0",
"target": { "input": "https://example.com", "type": "url", "host": "example.com", "port": null, "scheme": "https", "url": "https://example.com/" },
"status": "healthy",
"score": 97,
"started_at": "2026-08-11T10:00:00Z",
"duration_ms": 684,
"checks": {
"network": { "reachable": true, "ipv4": ["203.0.113.10"], "ipv6": ["2001:db8::10"], "ipv4_available": true, "ipv6_available": true },
"ports": { "80": { "port": 80, "status": "open", "latency_ms": 12, "error": null }, "443": { "port": 443, "status": "open", "latency_ms": 11, "error": null } },
"tls": { "enabled": true, "valid": true, "hostname_match": true, "protocol": "TLSv1.3", "certificate": { "subject": "example.com", "issuer": "Let's Encrypt", "valid_from": "2026-07-01T00:00:00Z", "valid_until": "2026-09-29T23:59:59Z", "days_remaining": 49, "san": ["example.com"] }, "failure": null, "error": null },
"http": { "reachable": true, "status_code": 200, "response_time_ms": 184, "protocol": "HTTP/2", "final_url": "https://example.com/", "redirects": 0, "redirect_chain": [{ "url": "https://example.com/", "status": 200 }], "redirect_loop": false, "headers": { "...": "..." }, "timing": { "dns_ms": 12, "tcp_connect_ms": 34, "tls_handshake_ms": 51, "ttfb_ms": 106, "total_ms": 184 } },
"security": { "https": true, "headers": { "hsts": { "header": "Strict-Transport-Security", "present": true, "value": "max-age=63072000" }, "csp": { "header": "Content-Security-Policy", "present": false, "value": null } } }
},
"issues": [
{ "code": "SECURITY_HEADER_MISSING", "severity": "warning", "message": "Content-Security-Policy header is missing", "check": "security", "context": { "key": "csp", "header": "Content-Security-Policy" } }
],
"metadata": { "generator": "1gbits/hostdoctor-laravel", "generator_version": "1.0.0", "safe_mode": true }
}
Issue codes
Codes are stable and machine-readable — branch on code, never on message.
| Code | Severity | Meaning |
|---|---|---|
DNS_RESOLUTION_FAILED |
critical | host has no A/AAAA records |
CONNECTION_REFUSED |
critical | primary TCP port closed |
CONNECTION_TIMEOUT |
critical | TCP/HTTP connection timed out |
HTTP_UNREACHABLE |
critical | HTTP request failed |
HTTP_UNEXPECTED_STATUS |
critical/warning | error status, or mismatch with expected_status |
HTTP_TOO_SLOW |
warning | response time above threshold |
REDIRECT_LOOP |
critical | redirect chain loops |
TOO_MANY_REDIRECTS |
warning | no final response within max_redirects |
TARGET_BLOCKED |
warning | redirect to a safe-mode-blocked target was not followed |
TLS_UNAVAILABLE |
critical | TLS handshake impossible |
TLS_INVALID |
critical | certificate not trusted |
TLS_EXPIRED |
critical | certificate expired |
TLS_EXPIRING_SOON |
warning | expires within tls.expiry_warning_days |
TLS_HOSTNAME_MISMATCH |
critical | certificate does not match hostname |
IPV6_UNAVAILABLE |
info | no AAAA records |
SECURITY_HEADER_MISSING |
warning/info | a well-known security header is absent |
Health score
The score starts at 100; each issue deducts configurable points (see config/hostdoctor.php → scoring). Missing security headers are capped at 10 points in total.
| Score | Status |
|---|---|
| 90–100 | healthy |
| 75–89 | warning |
| 50–74 | degraded |
| 0–49 | critical |
A report with any critical issue is never reported better than degraded, regardless of score.
// config/hostdoctor.php 'scoring' => [ 'deductions' => ['TLS_EXPIRING_SOON' => 10 /* ... */], 'status_thresholds' => ['healthy' => 90, 'warning' => 75, 'degraded' => 50], ],
Safe mode (SSRF protection) — on by default
HostDoctor opens connections to arbitrary targets, so it ships with safe_mode = true:
- targets that are — or resolve to — loopback, link-local (including cloud metadata
169.254.169.254), private, CGNAT, multicast or otherwise reserved addresses are rejected with aBlockedTargetException; - every redirect hop is re-validated, so a public URL cannot bounce the probe into your internal network;
- in safe mode, connections are pinned to the validated IP to resist DNS-rebinding tricks;
localhost,*.localhost,*.localand*.internalhostnames are always rejected.
Only disable it for trusted, internal tooling:
// config/hostdoctor.php 'safe_mode' => false, // or per instance: $internal = HostDoctor::withConfig(['safe_mode' => false]); $internal->port('10.0.0.5', 3306);
php artisan hostdoctor:check http://10.0.0.5 --unsafe
Other protections that are always on:
- response bodies are capped (
http.max_body_bytes, default 64 KB) — HostDoctor never downloads a 10 GB file to check a status code; - request headers such as
Authorizationare redacted ([REDACTED]) anywhere they appear in a report; - timeouts apply to every probe; only timeouts are retried (
retries, default 1) — an invalid certificate is never retried.
Use cases
Post-provisioning check — mark a VPS ready once SSH answers:
$check = HostDoctor::port($server->ip, 22); if ($check['status'] === 'open') { $server->markAsReady(); }
Deploy gate — fail the pipeline when production is unhealthy:
php artisan hostdoctor:check https://api.example.com --expect=200 || exit 1
Support triage — ask the customer for a machine-readable report:
php artisan hostdoctor:check https://customer-site.com --json
Testing your own code
HostDoctor is resolved from the container (hostdoctor / HostDoctor::class), and every probe sits behind a contract (OneGbits\HostDoctor\Contracts\*), so you can swap the whole service or individual probes:
$this->app->instance('hostdoctor', $myFakeHostDoctor); // or build one with fake probes: new HostDoctor([], $fakeNetwork, $fakeTcp, $fakeTls, $fakeHttp, $policy);
The package's own suite (98 tests) runs fully offline this way.
Roadmap
- 1.1 — batch checks, config-file/CI mode, JSON body expectations, richer retry policies
- 1.2 — probes for SSH banners, SMTP, MySQL/PostgreSQL, Redis, WebSocket
- 2.0 — plugin system for community probes
License
MIT © 1Gbits