Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

164 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ServiceHandler

ServiceHandler is a local web service registry with a web UI. It solves the problem of registering named services on the local machine so clients can look up each other by name, port, and metadata through a small HTTP API.

About

ServiceHandler is scoped to service registration and discovery on the local device. The service binds to 127.0.0.1 on port 49155 and rejects API calls that do not come from the local device. Registered clients are kept in memory only — each service must re-register every time ServiceHandler starts. A background health-check thread pings registered clients every 15 seconds and identifies unreachable ones.

The web UI serves two pages:

  • Dashboard (/) — ui/pages/index.html displays a status pill, a searchable and sortable grid of registered service cards, a sidebar for tweaking sort order and group-by, a health-check button, and checkbox-based batch selection for bulk actions.
  • Settings (/settings) — ui/pages/settings.html provides auto-protect and auto-restart configuration via tag-cloud inputs, along with shutdown and restart buttons. Includes a footer with a link to report issues on GitHub and a link to the repository author.

Page transitions use a fade-out animation triggered by the page-exit CSS class, with sessionStorage._navFromApp signalling the incoming page to skip initial hidden-state animations.

Features:

  • Search — real-time filtering with subsequence matching.
  • Filter panel — expandable filter menu with text inputs per column and a status dropdown (Any / Operational / Broken). Tab navigates in column-major order (top-to-bottom, then next column). Shift+Tab from the search input returns focus to the last filter.
  • Sort & Group-by — drag-to-reorder sort columns; group by a selected key; sort order persisted across restarts. The group-by zone is always visible regardless of card count.
  • Status grouping — services are automatically tagged as Operational or Broken; can be sorted/grouped by status.
  • Card expansion/collapse — click a card to expand it with full metadata; collapse returns the card to its grid position.
  • Health check — global button re-checks all services (shows a spinner, then updates card colours, buttons, and grid positions in-place). A background health-check loop runs automatically every 15 seconds. Per-card health endpoint refreshes expanded content in-place. When services are selected via checkboxes, health check runs only on the selected subset and clears the selection afterward.
  • Batch selection — hover over any card to reveal checkboxes; click to select individual cards. When one or more cards are selected, the search bar is replaced by Terminate Selected Services and Restart Selected Services buttons that span the card grid. Press Escape to clear selection.
  • Broken service management — broken services shown with red styling immediately. "Forget All Broken Services" and "Restart All Broken Services" buttons for bulk actions.
  • Auto-Protect — services matching names in the auto-protect list are automatically marked as protected on registration. Managed via the Settings page or GET/PUT /api/settings/auto-protect.
  • Auto-Restart — services matching names in the auto-restart list are automatically restarted when they fail a health check. Managed via the Settings page or GET/PUT /api/settings/auto-restart.
  • Shutdown & Restart buttons — pill-shaped buttons on the Settings page that send POST /api/shutdown to stop the ServiceHandler process or POST /api/restart to restart it.
  • Keyboard shortcuts — search auto-focused on load. Escape clears checkbox selection, closes expanded card, sort menu, or filter menu. Tab navigates filter inputs in column-major order.

Safety notice: ServiceHandler is intended only for environments where safety is not a major risk — the chances of malevolent actors are low, and the consequences of an eventual mishap are low.

Setup

  1. Install Python dependencies: pip install -r requirements.txt.
  2. Review resources/configuration.json if you want to change the port. Set SH_API_KEYS and other settings in .env (see below).
  3. Leave the project structure intact so the service can find resources/ and src/.

Environment Variables

Variable Description
SH_API_KEYS JSON object mapping service names to plain API keys. Loaded at session initialization. API keys are stored as plain text in .env.
SH_SORT_ORDER JSON array of column keys for the UI sort order. Persisted across restarts.
SH_GROUP_BY Key to group services by in the UI (e.g. protected, status). Persisted across restarts.
SH_ORIGINAL_SORT_ORDER JSON array for the ungrouped sort order baseline. Persisted across restarts.
SH_AUTO_PROTECT_SERVICES JSON array of service names that are automatically protected on registration. Persisted across restarts.
SH_AUTO_RESTART_SERVICES JSON array of service names that are automatically restarted when they fail a health check. Persisted across restarts.
SH_SHOW_PROMOTION Set to false to hide the promotion footer on the settings page. Defaults to true.

API Key Persistence (Optional)

ServiceHandler stores API keys as plain text in the SH_API_KEYS environment variable in .env. There is no encryption layer — API keys are stored and loaded as-is.

  1. Pre-configure persistent API keys via the SH_API_KEYS environment variable:

    SH_API_KEYS={"service_name":"<plain_text_api_key>"}
    

    The value is a JSON object where each key is a service name and each value is the plain text API key. On session initialization, ServiceHandler reads this env var and loads each key into memory.

  2. When a new API key is granted through the web UI or API, the response includes an env_var_entry string that can be copied directly into your .env file for persistence across restarts.

Headless Mode (Optional)

Set "guiEnabled": false in resources/configuration.json to disable all UI-related endpoints (/, /css/<path>, /ui/sort-settings). These routes return 404 when the flag is disabled. The API endpoints under /api/ remain fully operational.

Run

  1. Windows: run scripts\run.bat (add --verbose for debug output).
  2. Unix-like: run bash scripts/run.sh (add --verbose for debug output).
  3. Manual: run python src/main.py from the project root (add --verbose for debug output).

Access Control

All /api/* and /ui/* endpoints are local-device only. Requests from non-local addresses are rejected with:

  • 403 -> { "error": "Local device access only." }
  • All endpoints also support HEAD and OPTIONS.
  • API responses use Connection: close.

Sensitive endpoints require a valid API key, with these exceptions:

Restriction level Endpoints
API key, localhost, or self-service (POST) — accepts if any of: valid API key, localhost, or service acting on itself (hash is proof of identity) /api/service/terminate, /api/service/restart, /api/unregister/service, /api/broken/forget, /api/broken/restart, /api/services/healthcheck
Strict localhost or valid API key (GET) — localhost allowed for bootstrapping /api/api-key/pending
Strict localhost or valid API key (POST + involving_api_keys) — localhost allowed for bootstrapping /api/api-key/grant, /api/api-key/reject
Valid API key or localhost (POST) /api/shutdown, /api/restart
Optional API key — returns full data if authorized, basic data otherwise POST /api/question/service, GET /api/services
Hash-only auth — service must provide its own hash, no API key option POST /api/register/endpoint
No auth required POST /api/service/endpoints, POST /api/endpoints/service, GET /api/services/endpoints, POST /api/validate/json-body

All endpoints that accept a hash parameter use _check_authorization_all, which grants access if: a valid API key is provided, the request comes from localhost, or the hash corresponds to a known registered service (self-service — knowing the hash is proof of identity). ServiceHandler's own localhost requests are also accepted.

When authorization fails, the response is 403 with { "error": "API key is not valid." }. Non-local requests are rejected at the firewall level before any endpoint-specific auth runs, returning 403 with { "error": "Local device access only." }.

These restrictions apply to the main HTTP method only; HEAD and OPTIONS are unaffected.

API Endpoints

GET / (also HEAD, OPTIONS)

Serves the web UI dashboard (ui/pages/index.html).

  • Auth: local-device only (no API key required)
  • Body: none
  • Returns:
    • 200 -> text/html

GET /settings (also HEAD, OPTIONS)

Serves the settings page (ui/pages/settings.html) with auto-protect and auto-restart configuration widgets and a shutdown button.

  • Auth: local-device only (no API key required)
  • Body: none
  • Returns:
    • 200 -> text/html

GET /css/<path:filename> (also HEAD, OPTIONS)

Serves static CSS files from the ui/css/ directory.

  • Auth: local-device only (no API key required)
  • Path parameters:
    • filename (string, required): path to a CSS file relative to ui/css/.
  • Body: none
  • Returns:
    • 200 -> text/css
    • 404 -> HTML error page

POST /api/register/service (also HEAD, OPTIONS)

Registers a new client service and returns a SHA-256 hash. Before registering, ServiceHandler probes the new client's health endpoint (/api/health) to confirm it is reachable.

  • Auth: local-device only (no API key required)

  • Body (JSON object):

    • name (string, required): name for the client service.
    • port (number, required): port number the client listens on (1–65535).
    • bind_address (string, required): IP address the client binds to.
    • hostname (string, required): hostname of the client machine.
    • starting_script (string, optional): path to the client's startup script.

    If a client with the same name is already registered, ServiceHandler checks whether that existing client is still alive. If it is, registration is rejected. If it is not, the stale registration is replaced.

    The recommended value for starting_script is the OS-appropriate run script — scripts/run.bat on Windows or scripts/run.sh on Unix — not the main.py file directly.

  • Returns:

    • 201 ->
       { "hash": "<sha256-hash>" }
    • 400 -> { "error": "A non-empty name is required." }
    • 400 -> { "error": "A port number is required." }
    • 400 -> { "error": "Port must be a number between 1 and 65535." }
    • 400 -> { "error": "A bind address is required." }
    • 400 -> { "error": "A hostname is required." }
    • 400 -> { "error": "Client health endpoint is not reachable." }
    • 409 -> { "error": "A client with name '...' is already registered." }

POST /api/question/service (also HEAD, OPTIONS)

Looks up a registered client's data by name. The service name can be passed either as a URL path parameter (/api/question/service/<name>) or in the JSON body. No registration is required to ask.

  • Auth: optional API key — returns full data if authorized (valid API key or localhost), basic data (name + port) otherwise
  • Body (JSON object):
    • name (string, optional if provided in path): name of the target client to look up.
    • api_key (string, optional): API key for full data access.
  • Returns (no API key or unauthorized):
    • 200 ->
       {
       	"name": "<target-name>",
       	"port": <target-port>
       }
  • Returns (valid API key):
    • 200 ->
       {
       	"hash": "<sha256>",
       	"name": "<target-name>",
       	"port": 8080,
       	"starting_script": "scripts/run.bat",
       	"pid": 12345,
       	"bind_address": "127.0.0.1",
       	"hostname": "my-host",
       	"ip": "127.0.0.1",
       	"timestamp": "2025-01-01T00:00:00"
       }
    • 400 -> { "error": "The name of the target client is required." }
    • 403 -> { "error": "API key is not valid." }
    • 404 -> { "error": "No client found with name '...'." }

DELETE /api/unregister/service (also HEAD, OPTIONS)

Unregisters a client by its hash.

  • Auth: API key, localhost, or self-service (any registered service that knows its own hash)
  • Body (JSON object):
    • hash (string, required): SHA-256 hash of the client to unregister.
    • api_key (string, optional): API key to authenticate the request from a non-localhost client.
  • Returns:
    • 200 ->
       {
       	"status": "unregistered",
       	"hash": "<hash>"
       }
    • 400 -> { "error": "A hash is required to unregister." }
    • 403 -> { "error": "API key is not valid." }
    • 404 -> { "error": "Hash not found." }

GET /api/health (also HEAD, OPTIONS)

ServiceHandler's own health check with registration statistics.

  • Auth: local-device only (no API key required)
  • Body: none
  • Returns:
    • 200 ->
       {
       	"status": "ok",
       	"service": "ServiceHandler",
       	"bind_address": "127.0.0.1",
       	"port": 49155,
       	"pid": 12345,
       	"hostname": "workstation-name",
       	"registered_clients": 0
       }

GET /api/services (also HEAD, OPTIONS)

Returns the list of all registered clients. The endpoints field is never included — use GET /api/services/endpoints for endpoint data.

  • Auth: optional API key — returns full data including hashes if authorized (valid API key or localhost), basic data without hashes otherwise
  • Body (JSON object):
    • api_key (string, optional): API key to receive full client data including hashes.
  • Returns (unauthorized):
    • 200 ->
       {
       	"clients": [
       		{
       			"name": "my-service",
       			"port": 8080,
       			"pid": 12345,
       			"ip": "127.0.0.1",
       			"timestamp": "2025-01-01T00:00:00",
       			"starting_script": "scripts/run.bat",
       			"bind_address": "127.0.0.1",
       			"hostname": "my-host"
       		}
       	]
       }
  • Returns (localhost or valid API key):
    • 200 ->
       {
       	"clients": [
       		{
       			"hash": "<sha256>",
       			"name": "my-service",
       			"port": 8080,
       			"pid": 12345,
       			"ip": "127.0.0.1",
       			"timestamp": "2025-01-01T00:00:00",
       			"starting_script": "scripts/run.bat",
       			"bind_address": "127.0.0.1",
       			"hostname": "my-host"
       		}
       	]
       }
    • 403 -> { "error": "API key is not valid." } (only when an invalid API key is explicitly provided)

POST /api/register/endpoint (also HEAD, OPTIONS)

Registers an endpoint for a registered service. The service authenticates by providing its own hash — knowing the hash is proof of identity. No API key option is available.

  • Auth: hash-only — the hash must correspond to a registered service
  • Body (JSON object):
    • hash (string, required): SHA-256 hash of the service registering the endpoint.
    • verb (string, required): HTTP verb for the endpoint (e.g. GET, POST).
    • path (string, required): URL path of the endpoint.
    • path_variables (array of strings, optional): list of variable names used in the path.
    • body_schema (object, optional): JSON schema for the request body.
    • description (string, required): human-readable description of what the endpoint does.
  • Returns:
    • 201 ->
       {
       	"status": "registered",
       	"endpoint": {
       		"verb": "GET",
       		"path": "/api/data",
       		"path_variables": ["id"],
       		"body_schema": {},
       		"description": "Retrieves data by ID"
       	}
       }
    • 400 -> { "error": "A hash is required." }
    • 400 -> { "error": "A non-empty HTTP verb is required." }
    • 400 -> { "error": "A non-empty endpoint path is required." }
    • 400 -> { "error": "A non-empty description is required." }
    • 404 -> { "error": "Service not found." }

POST /api/service/endpoints / POST /api/endpoints/service (also HEAD, OPTIONS)

Returns the list of endpoints registered for a given service name. The service name can be passed either as a URL path parameter (/api/service/endpoints/<name> or /api/endpoints/service/<name>) or in the JSON body.

  • Auth: none
  • Body (JSON object):
    • name (string, optional if provided in path): name of the registered service.
  • Returns:
    • 200 ->
       {
       	"name": "my-service",
       	"endpoints": [
       		{
       			"verb": "GET",
       			"path": "/api/data",
       			"path_variables": ["id"],
       			"body_schema": {},
       			"description": "Retrieves data by ID"
       		}
       	]
       }
    • 400 -> { "error": "The name of the service is required." }
    • 404 -> { "error": "No service found with name '...'." }

GET /api/services/endpoints (also HEAD, OPTIONS)

Returns the complete list of registered services along with their registered endpoints. Each entry only includes name, ip, port, and endpoints.

  • Auth: none
  • Body: none
  • Returns:
    • 200 ->
       {
       	"clients": [
       		{
       			"name": "my-service",
       			"ip": "127.0.0.1",
       			"port": 8080,
       			"endpoints": [
       				{
       					"verb": "GET",
       					"path": "/api/data",
       					"path_variables": ["id"],
       					"body_schema": {},
       					"description": "Retrieves data by ID"
       				}
       			]
       		}
       	]
       }

POST /api/services/search-endpoints (also HEAD, OPTIONS)

Searches endpoint descriptions across all registered services, returning matches with the service name.

  • Auth: none
  • Body (JSON object):
    • query (string, required): search term to match against endpoint descriptions (case-insensitive substring match).
  • Returns:
    • 200 ->
       {
       	"query": "health",
       	"results": [
       		{
       			"service": "my-service",
       			"verb": "GET",
       			"path": "/api/health",
       			"description": "Service health check",
       			"path_variables": [],
       			"body_schema": {}
       		}
       	]
       }
    • 400 -> {"error": "A non-empty query is required."}

POST /api/validate/json-body (also HEAD, OPTIONS)

Validates a JSON body against the JSON schema of a registered endpoint. The response indicates whether a schema exists, whether the body is valid, and provides validation errors if the body is invalid.

  • Auth: none
  • Body (JSON object):
    • service (string, required): name of the registered service.
    • verb (string, required): HTTP verb of the target endpoint (e.g. GET, POST).
    • path (string, required): URL path of the target endpoint.
    • json_body (object, required): JSON body to validate against the endpoint's schema.
  • Returns (no schema):
    • 200 ->
       {
       	"valid": true,
       	"schema_exists": false,
       	"message": "No JSON schema defined for this endpoint."
       }
  • Returns (valid):
    • 200 ->
       {
       	"valid": true,
       	"schema_exists": true,
       	"message": "JSON body is valid against the endpoint schema."
       }
  • Returns (invalid):
    • 200 ->
       {
       	"valid": false,
       	"schema_exists": true,
       	"message": "JSON body is not valid against the endpoint schema.",
       	"errors": [
       		{
       			"path": ["field", "nested"],
       			"message": "'...' is a required property"
       		}
       	]
       }
    • 400 -> { "error": "A non-empty service name is required." }
    • 400 -> { "error": "A non-empty HTTP verb is required." }
    • 400 -> { "error": "A non-empty endpoint path is required." }
    • 400 -> { "error": "A json_body is required." }
    • 404 -> { "error": "No service found with name '...'." }
    • 404 -> { "error": "No endpoint found with verb '...' and path '...' for service '...'." }

POST /api/validate-key (also HEAD, OPTIONS)

Validates whether a given API key exists in the server's key store. Useful for clients to verify a key before using it.

  • Auth: local-device only (no API key required)
  • Body (JSON object):
    • api_key (string, required): the API key to validate.
  • Returns (valid):
    • 200 ->
       {
       	"valid": true
       }
  • Returns (invalid):
    • 200 ->
       {
       	"valid": false
       }
    • 400 -> { "error": "A non-empty api_key is required." }

GET /ui/sort-settings (also HEAD, OPTIONS)

Returns the current column sort order and group-by key used by the web UI.

  • Auth: local-device only (no API key required)
  • Body: none
  • Returns:
    • 200 ->
       {
       	"sort_order": ["name", "port", "pid", "bind_address", "hostname", "status"],
       	"group_by": "name",
       	"original_sort_order": ["name", "port", "pid", "bind_address", "hostname", "status"]
       }

PUT /ui/sort-settings (also HEAD, OPTIONS)

Updates the column sort order and/or group-by key.

  • Auth: local-device only (no API key required)
  • Body (JSON object):
    • sort_order (array of strings, optional): column keys in desired order.
    • group_by (string or null, optional): key to group by, or null to ungroup.
    • original_sort_order (array of strings, optional): baseline sort order for the ungrouped view.
  • Returns:
    • 200 ->
       {
       	"sort_order": ["port", "name", "pid"],
       	"group_by": "name",
       	"original_sort_order": ["port", "name", "pid"]
       }
    • 400 -> { "error": "sort_order must be a non-empty list." }
    • 500 -> { "error": "Failed to read/write configuration." }

POST /api/service/terminate (also HEAD, OPTIONS)

Terminates a registered client process and unregisters it.

  • Auth: API key, localhost, or self-service (any registered service that knows its own hash)
  • Body (JSON object):
    • hash (string, required): SHA-256 hash of the client to terminate.
    • pid (number, optional): process ID to kill. If omitted, the server looks up the stored PID for the client.
    • api_key (string, optional): API key to authenticate the request from a non-localhost client (not needed for self-service or localhost).
  • Returns:
    • 200 ->
       {
       	"status": "terminated",
       	"hash": "<hash>",
       	"pid": 12345
       }
    • 400 -> { "error": "A hash is required." }
    • 400 -> { "error": "No PID available for this service." }
    • 403 -> { "error": "API key is not valid." }
    • 500 -> { "error": "Failed to terminate process: ..." }

POST /api/service/restart (also HEAD, OPTIONS)

Restarts a registered client process via its start script. The starting script and PID are looked up from the server's stored client data.

  • Auth: API key, localhost, or self-service (any registered service that knows its own hash)
  • Body (JSON object):
    • hash (string, required): SHA-256 hash of the client.
    • api_key (string, optional): API key to authenticate the request from a non-localhost client (not needed for self-service or localhost).
  • Returns:
    • 200 ->
       {
       	"status": "restarted",
       	"hash": "<hash>"
       }
    • 400 -> { "error": "A hash is required." }
    • 400 -> { "error": "Client not found." }
    • 400 -> { "error": "No starting script available for this service." }
    • 400 -> { "error": "No PID available for this service." }
    • 403 -> { "error": "API key is not valid." }
    • 500 -> { "error": "Failed to terminate process: ..." }
    • 500 -> { "error": "Failed to start script: ..." }

POST /api/broken/forget (also HEAD, OPTIONS)

Removes a client from the broken list without requiring a termination. If the client is still registered, it is also unregistered and its process is killed if possible.

  • Auth: API key, localhost, or self-service (any registered service that knows its own hash)
  • Body (JSON object):
    • hash (string, required): SHA-256 hash of the broken client.
    • api_key (string, optional): API key to authenticate the request from a non-localhost client.
  • Returns:
    • 200 ->
       {
       	"status": "forgotten",
       	"hash": "<hash>"
       }
    • 400 -> { "error": "A hash is required." }
    • 403 -> { "error": "API key is not valid." }

POST /api/broken/restart (also HEAD, OPTIONS)

Forgets a client from the broken list, kills its process if still running, then restarts it via its start script. The starting script is looked up from the server's stored client data.

  • Auth: API key, localhost, or self-service (any registered service that knows its own hash)
  • Body (JSON object):
    • hash (string, required): SHA-256 hash of the broken client.
    • api_key (string, optional): API key to authenticate the request from a non-localhost client.
  • Returns:
    • 200 ->
       {
       	"status": "restarted",
       	"hash": "<hash>"
       }
    • 400 -> { "error": "A hash is required." }
    • 400 -> { "error": "No starting script available for this service." }
    • 403 -> { "error": "API key is not valid." }
    • 500 -> { "error": "Failed to start script: ..." }

POST /api/services/healthcheck (also HEAD, OPTIONS)

Checks the health of a specific client by hash, or all registered clients if no hash is given.

  • Auth: API key, localhost, or self-service (any registered service that knows its own hash — only applies when checking a specific hash)
  • Body (JSON object):
    • hash (string, optional): SHA-256 hash of a specific client to check. If omitted, all registered clients are checked.
    • api_key (string, optional): API key to authenticate the request from a non-localhost client.
  • Returns (single client):
    • 200 ->
       {
       	"hash": "<sha256>",
       	"healthy": true
       }
      or
       {
       	"hash": "<sha256>",
       	"healthy": false
       }
    • 404 -> { "error": "Client not found." }
  • Returns (all clients):
    • 200 ->
       {
       	"checked": true,
       	"unhealthy": [
       		{
       			"hash": "<sha256>",
       			"name": "my-service",
       			"port": 8080,
       			"pid": 12345,
       			"ip": "127.0.0.1",
       			"timestamp": "2025-01-01T00:00:00",
       			"starting_script": "scripts/run.bat",
       			"bind_address": "127.0.0.1",
       			"hostname": "my-host"
       		}
       	]
       }
    • 403 -> { "error": "API key is not valid." }

POST /api/shutdown (also HEAD, OPTIONS)

Shuts down the ServiceHandler service.

  • Auth: API key or localhost
  • Body (JSON object):
    • api_key (string, optional): API key to authenticate the request from a non-localhost client.
  • Returns:
    • 200 ->
       {
       	"status": "shutdown"
       }
    • 403 -> { "error": "API key is not valid." }

POST /api/restart (also HEAD, OPTIONS)

Restarts the ServiceHandler service by spawning a new process before shutting down the current one.

  • Auth: API key or localhost
  • Body (JSON object):
    • api_key (string, optional): API key to authenticate the request from a non-localhost client.
  • Returns:
    • 200 ->
       {
       	"status": "restart"
       }
    • 403 -> { "error": "API key is not valid." }

GET /api/settings/auto-protect (also HEAD, OPTIONS)

Returns the list of service names that are automatically protected on registration.

  • Auth: local-device only (no API key required)
  • Body: none
  • Returns:
    • 200 ->
       {
       	"services": ["service-a", "service-b"]
       }

PUT /api/settings/auto-protect (also HEAD, OPTIONS)

Updates the list of service names that are automatically protected on registration. The provided services are sorted alphabetically and duplicate names (after stripping whitespace) are rejected.

  • Auth: local-device only (no API key required)
  • Body (JSON object):
    • services (array of strings, required): list of service names.
  • Returns:
    • 200 ->
       {
       	"services": ["service-a", "service-b"]
       }
    • 400 -> { "error": "services must be a non-empty list." }
    • 400 -> { "error": "Duplicate service names are not allowed." }
    • 400 -> { "error": "Each service name must be a non-empty string." }

GET /api/settings/auto-restart (also HEAD, OPTIONS)

Returns the list of service names that are automatically restarted when they fail a health check.

  • Auth: local-device only (no API key required)
  • Body: none
  • Returns:
    • 200 ->
       {
       	"services": ["service-a", "service-b"]
       }

PUT /api/settings/auto-restart (also HEAD, OPTIONS)

Updates the list of service names that are automatically restarted when they fail a health check. The provided services are sorted alphabetically and duplicate names (after stripping whitespace) are rejected.

  • Auth: local-device only (no API key required)
  • Body (JSON object):
    • services (array of strings, required): list of service names.
  • Returns:
    • 200 ->
       {
       	"services": ["service-a", "service-b"]
       }
    • 400 -> { "error": "services must be a non-empty list." }
    • 400 -> { "error": "Duplicate service names are not allowed." }
    • 400 -> { "error": "Each service name must be a non-empty string." }

GET /api/settings/show-promotion (also HEAD, OPTIONS)

Returns whether the promotion footer is shown on the settings page.

  • Auth: local-device only (no API key required)
  • Body: none
  • Returns:
    • 200 ->
       {
       	"show_promotion": true
       }

PUT /api/settings/show-promotion (also HEAD, OPTIONS)

Updates whether the promotion footer is shown on the settings page.

  • Auth: local-device only (no API key required)
  • Body (JSON object):
    • show_promotion (boolean, required): true to show the footer, false to hide it.
  • Returns:
    • 200 ->
       {
       	"show_promotion": true
       }
    • 400 -> { "error": "show_promotion must be a boolean." }

GET /resources/<path:filename> (also HEAD, OPTIONS)

Serves static resource files from the resources/ directory.

  • Auth: local-device only (no API key required)
  • Path parameters:
    • filename (string, required): path to a file relative to resources/.
  • Returns:
    • 200 -> file content
    • 404 -> HTML error page

POST /api/service/protect (also HEAD, OPTIONS)

Flags a registered service as protected. Protected services cannot be terminated, restarted, or forgotten by anyone.

  • Auth: valid API key or localhost
  • Body (JSON object):
    • hash (string, required): SHA-256 hash of the service to protect.
    • api_key (string, optional): API key to authenticate the request from a non-localhost client.
  • Returns:
    • 200 ->
       {
       	"status": "protected",
       	"hash": "<sha256>"
       }
    • 400 -> { "error": "A hash is required." }
    • 403 -> { "error": "API key is not valid." }
    • 404 -> { "error": "Client not found." }

POST /api/service/unprotect (also HEAD, OPTIONS)

Removes the protected flag from a registered service, allowing it to be terminated, restarted, or forgotten again.

  • Auth: valid API key or localhost
  • Body (JSON object):
    • hash (string, required): SHA-256 hash of the service to unprotect.
    • api_key (string, optional): API key to authenticate the request from a non-localhost client.
  • Returns:
    • 200 ->
       {
       	"status": "unprotected",
       	"hash": "<sha256>"
       }
    • 400 -> { "error": "A hash is required." }
    • 403 -> { "error": "API key is not valid." }
    • 404 -> { "error": "Client not found." }

POST /api/api-key/request (also HEAD, OPTIONS)

Submits an API key request for a registered client. The request enters a pending queue for the device owner to approve.

  • Auth: hash-only — the hash must correspond to a registered service (no API key option)
  • Body (JSON object):
    • hash (string, required): SHA-256 hash of the registered client requesting an API key.
  • Returns:
    • 201 ->
       {
       	"status": "pending",
       	"message": "API key request registered. Awaiting approval."
       }
    • 200 -> { "status": "already_pending", "message": "API key request is already pending." }
    • 400 -> { "error": "A hash is required." }
    • 404 -> { "error": "Client not found." }
    • 503 -> { "error": "..." } (API key session not available)

GET /api/api-key/pending (also HEAD, OPTIONS)

Lists all pending API key requests with full details and an array of hashes.

  • Auth: localhost or valid API key
  • Body (JSON object):
    • api_key (string, optional): API key to authenticate the request from a non-localhost client.
  • Returns:
    • 200 ->
       {
       	"pending": [
       		{
       			"hash": "<sha256>",
       			"name": "my-service",
       			"port": 8080,
       			"ip": "127.0.0.1",
       			"timestamp": "2025-01-01T00:00:00"
       		}
       	],
       	"hashes": ["<sha256>", "<sha256>"]
       }
    • 403 -> { "error": "API key is not valid." }

POST /api/api-key/grant (also HEAD, OPTIONS)

Approves a pending API key request, generates a key, and notifies the requesting service.

  • Auth: localhost or valid API key
  • Body (JSON object):
    • hash (string, required): SHA-256 hash from the pending request to grant.
    • api_key (string, optional): API key to authenticate the request from a non-localhost client.
  • Returns:
    • 200 ->
       {
       	"status": "granted",
       	"api_key": "<128-hex-chars>",
       	"service": "my-service",
       	"notified": true
       }
    • 400 -> { "error": "A hash is required." }
    • 403 -> { "error": "API key is not valid." }
    • 404 -> { "error": "No pending API key request for this client." }
    • 500 -> { "error": "Failed to persist API key." }
    • 503 -> { "error": "..." } (API key session not available)

POST /api/api-key/reject (also HEAD, OPTIONS)

Rejects a pending API key request and notifies the requesting service.

  • Auth: localhost or valid API key
  • Body (JSON object):
    • hash (string, required): SHA-256 hash from the pending request to reject.
    • api_key (string, optional): API key to authenticate the request from a non-localhost client.
  • Returns:
    • 200 ->
       {
       	"status": "rejected",
       	"service": "my-service",
       	"notified": true
       }
    • 400 -> { "error": "A hash is required." }
    • 403 -> { "error": "API key is not valid." }
    • 404 -> { "error": "No pending API key request for this client." }

Support

  • Open an issue on GitHub for bug reports, feature requests, or help.

License

Author

About

REST API for automating local web-services port routing.

Topics

Resources

Security policy

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages