Skip to content

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 ​

typescript
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.

typescript
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

FieldTypeDescription
routesRouteDefinition[]The route table, matched in order
middlewareHttpHandler[]Runs before every route, in order
notFoundHttpHandlerServed 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.

typescript
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.

typescript
// 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.

HelperStatus
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 ​

FieldTypeDescription
pathstringSupports :param segments and a trailing * wildcard
methodHttpMethod | HttpMethod[] | "*"Defaults to any method
handlerHttpHandlerAnswers the request

HttpRequest ​

FieldTypeDescription
idstring
methodHttpMethodGET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
urlstringFull URL as issued, including scheme, port and query
hoststringHostname without port, lowercased, www. stripped
portnumber
pathstringPath only, no query
queryRecord<string, string>
headersRecord<string, string>Names are always lowercase, so lookups never guess casing
bodystring
originHttpOriginbrowser, terminal, proxy, server or script
atnumberIn-game timestamp
incognitoboolean?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.

MemberTypeDescription
paramsRecord<string, string>Values captured from :param segments
cookiesRecord<string, string>Cookies the client sent
json<T>()T | nullParsed 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 ​

FieldTypeDescription
idstring
hoststringThe subdomain that was hit
pathstring
methodstring
kind"dns" | "http"dns for a lookup with nothing behind it, http for a full request
headersRecord<string, string>
bodystring
atnumberIn-game timestamp

Full example ​

A target with a real access-control bug in it:

typescript
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");

Every simulated exchange raises events, whoever made it: the browser, curl, the proxy replaying something, or one server calling another.

EventPayload
Http.RequestHttpRequest
Http.ResponseHttpTransaction
Http.InterceptedHttpRequest
Http.CollaboratorHitCollaboratorHit

See the Events guide for how to listen.

HotBunny Interactive Entertainment Inc.