Http
The Http namespace lets content stand up web applications the player can actually browse, probe and attack over the game's simulated network.
Requires the network permission.
Available since SDK v0.23.0
The Http namespace requires @hotbunny/[email protected] or newer. Run npm install @hotbunny/hackhub-content-sdk@latest to update.
Http vs Website
The difference from Website matters.
A Website is a page: the browser already has the content and renders it. A host registered here is a server: the browser asks it for every page, the player's proxy watches the exchange, and the same endpoint answers a request crafted by hand in a tool. That is what makes a target something you can hunt in rather than something you can read.
Reach for Website for a site that only needs to be read. Reach for Http when the site is the target.
Import
import { Http } from "@hotbunny/hackhub-content-sdk";Servers
Http.registerHost(host, handler)
Serve host with handler. Pair it with Network.registerDomain so the host also resolves for nslookup, ping and the firewall.
Http.registerHost("shop.example", Http.createServer({
routes: [{ path: "/", handler: () => Http.html("<h1>Shop</h1>") }],
}));
Network.registerDomain("shop.example", "10.20.0.14");Http.registerWildcardHost(suffix, handler)
Serve every subdomain of suffix. Meant for out-of-band listeners, where the subdomain is chosen by the payload rather than known in advance.
Http.unregisterHost(host)
Stop serving a host.
Http.hasHost(host)
Returns: boolean
Http.createServer(options)
Build a handler from a route table.
Returns: HttpHandler
| Field | Type | Description |
|---|---|---|
routes | RouteDefinition[] | The route table, matched in order |
middleware | HttpHandler[] | Runs before every route, in order |
notFound | HttpHandler | Served when no route matched. Defaults to a plain 404 |
Middleware runs first, in order, and can answer on its own: that is where a WAF, a rate limiter or an authentication gate goes. Returning a response short-circuits the request.
An endpoint that exists but not for the requested verb answers 405 rather than 404, because telling a hunter the path is real is information a real server leaks too.
Http.publish(host, listing) / Http.unpublish(host)
Let the in-game search engine index a host you serve.
Registering a host puts a server on the network; it does not put the site on the web the way a player experiences it. Nothing in the game knows the place exists until someone types the domain, which is correct for a target meant to be discovered and wrong for anywhere the player is told to go.
WARNING
Only publish what a player could plausibly look up. A vulnerable staging box that turns up in search has given the game away.
Http.publish("hackernone.test", {
siteName: "HackerNone",
description: "Coordinated disclosure, bug bounty programmes and hacktivity.",
search: ["hackernone", "bug bounty", "vulnerability disclosure"],
pages: [{ path: "/programs", title: "Programs - HackerNone" }],
});Client
Http.fetch(url, init?)
Issue a request over the simulated network. Goes through DNS, the firewall and the player's proxy exactly like a request the player made.
Returns: Promise<HttpResponse>
Http.history() / Http.clearHistory()
Recent exchanges, oldest first. This is what the proxy's history shows.
Returns: HttpTransaction[]
Http.isRecording() / Http.setRecording(enabled)
Whether exchanges are being written to history.
Off until something turns it on, because a machine with no proxy running on it keeps no log. A pack that ships a proxy owns this switch: turn it on when the tool opens and off when it closes, and the player's history is the session they were actually working in rather than everything since the save was created.
It gates the log only. Http.Response still fires either way, so a quest that watches traffic keeps working with recording off.
// In the proxy app, on open and on close.
Http.setRecording(true);
window.addEventListener("pagehide", () => Http.setRecording(false));Http.historyLimit() / Http.setHistoryLimit(limit)
How many exchanges history keeps. Lowering it trims immediately.
Cookies
Http.getCookies(host)
Cookies a client would send to host.
Returns: Record<string, string>
Http.setCookie(host, name, value, options?)
Set a cookie in the player's jar. Options are httpOnly, path and expiresIn.
httpOnly is not decoration: a cookie marked so is hidden from page script, which is the whole difference between a session an XSS payload can steal and one it cannot.
Http.clearCookies(host?)
Clear one host's cookies, or all of them.
Out-of-band
Http.registerCollaborator(baseDomain)
Claim a domain whose subdomains all answer and are logged.
This is the listener half of a blind finding. A payload that only ever fires on someone else's machine leaves no trace on screen; a request arriving here is the only evidence it worked.
Http.mintCollaboratorSubdomain()
A fresh subdomain, so one payload's callback can be told from another's.
Returns: string
Http.collaboratorHits(host?)
Interactions received, newest first. Pass a host to filter to one payload.
Returns: CollaboratorHit[]
Intercept
Hold requests in flight so they can be inspected and rewritten before they reach the server. This is the engine half of a proxy tool.
Whatever turns this on must be able to turn it off
A held request is a real request waiting on a real answer. Nothing else in the game will release the queue, and a player who cannot reach the tool that is holding their traffic just sees a browser that stopped working. The engine forces a held request through after two minutes for that reason, but a timeout is a safety net, not a design.
Requests a server made are never held: a proxy on the player's machine cannot see one machine call another, and showing it would make blind bugs sighted.
Http.interceptEnabled() / Http.setInterceptEnabled(enabled)
Http.interceptQueue()
Requests waiting on the player, oldest first.
Returns: HeldRequest[]
Http.interceptForward(id, replacement?)
Let a held request continue, as itself or as something else entirely. replacement is the point of intercepting at all: what reaches the server is whatever the player edited, headers and body included.
Http.interceptDrop(id)
Kill a held request. The caller sees a dropped connection.
Http.interceptForwardAll() / Http.interceptDropAll()
Response helpers
Each returns an HttpResponseInit with the right status and content type, so a handler rarely writes headers by hand.
| Helper | Status |
|---|---|
Http.text(body, status?) | 200 |
Http.html(body, status?) | 200 |
Http.json(data, status?) | 200 |
Http.notFound(message?) | 404 |
Http.unauthorized(message?) | 401 |
Http.forbidden(message?) | 403 |
Http.redirect(location, status?) | 302 |
Http.serverError(message?) | 500 |
Types
RouteDefinition
| Field | Type | Description |
|---|---|---|
path | string | Supports :param segments and a trailing * wildcard |
method | HttpMethod | HttpMethod[] | "*" | Defaults to any method |
handler | HttpHandler | Answers the request |
HttpRequest
| Field | Type | Description |
|---|---|---|
id | string | |
method | HttpMethod | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS |
url | string | Full URL as issued, including scheme, port and query |
host | string | Hostname without port, lowercased, www. stripped |
port | number | |
path | string | Path only, no query |
query | Record<string, string> | |
headers | Record<string, string> | Names are always lowercase, so lookups never guess casing |
body | string | |
origin | HttpOrigin | browser, terminal, proxy, server or script |
at | number | In-game timestamp |
incognito | boolean? | Set when the request comes from a private browser window. A site that knows the player from the save rather than a session cookie should serve itself signed out. Cookie-based sites need nothing: a private window has its own empty cookie jar |
HttpContext
The second argument to every handler.
| Member | Type | Description |
|---|---|---|
params | Record<string, string> | Values captured from :param segments |
cookies | Record<string, string> | Cookies the client sent |
json<T>() | T | null | Parsed JSON body, or null when there isn't one |
form() | Record<string, string> | Parsed form-encoded body |
fetch(url, init?) | Promise<HttpResponse> | Issue a request from this server |
context.fetch is what makes a server-side request forgery a real event rather than a scripted one: a handler that fetches a URL the player supplied genuinely reaches wherever that URL points.
HttpResponse
status, statusText, headers, body, and timeMs for the simulated round trip.
HttpTransaction
A request and its response, plus edited when the player changed the request in the proxy before it went out, and synthetic when the browser reconstructed it for a page nothing actually served.
CollaboratorHit
| Field | Type | Description |
|---|---|---|
id | string | |
host | string | The subdomain that was hit |
path | string | |
method | string | |
kind | "dns" | "http" | dns for a lookup with nothing behind it, http for a full request |
headers | Record<string, string> | |
body | string | |
at | number | In-game timestamp |
Full example
A target with a real access-control bug in it:
import { Http, Network } from "@hotbunny/hackhub-content-sdk";
const orders: Record<string, { user: string; total: number }> = {
1041: { user: "you", total: 39.9 },
1042: { user: "someone else", total: 512 },
};
Http.registerHost("shop.example", Http.createServer({
middleware: [
// A WAF that only looks at the query string. Also the way past it.
(req) => (req.query.q?.includes("<script") ? Http.forbidden("Blocked") : undefined),
],
routes: [
{ path: "/", handler: () => Http.html("<h1>Shop</h1>") },
{
path: "/api/orders/:id",
method: "GET",
// No check that the order belongs to the caller. That is the bug.
handler: (_req, ctx) => Http.json(orders[ctx.params.id] ?? null),
},
],
}));
Network.registerDomain("shop.example", "10.20.0.14");Related events
Every simulated exchange raises events, whoever made it: the browser, curl, the proxy replaying something, or one server calling another.
| Event | Payload |
|---|---|
Http.Request | HttpRequest |
Http.Response | HttpTransaction |
Http.Intercepted | HttpRequest |
Http.CollaboratorHit | CollaboratorHit |
See the Events guide for how to listen.
