hostdoctor-laravel maintained by 1gbits
HostDoctor for Laravel
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