1gbits/hostdoctor-laravel

Health checks for hosts, servers and web services in Laravel. HTTP, TLS, TCP, DNS, security headers and a normalized health score.

Maintainers

Package info

github.com/1gbitsofficial/hostdoctor-laravel

pkg:composer/1gbits/hostdoctor-laravel

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-11 11:45 UTC

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, openssl and json extensions
  • 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.phpscoring). 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 a BlockedTargetException;
  • 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, *.local and *.internal hostnames 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 Authorization are 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