Unlike PennMUSH, SharpMUSH pre-populates the HTTP handler: a new database is seeded with an HTTP Handler object (#8), the “http_handler” config already points at it, and the default verb attributes (&GET, &POST, &PUT, &DELETE, &PATCH, &HEAD) are already installed on it — see [http routing]. You extend the API by adding routed sub-attributes, not by creating a handler. This is low level, and a little tricky to understand.
If the HTTP Handler is unset, or no matching method/route attribute exists on the handler object, SharpMUSH responds with a plain 404 Not Found.
@config http_per_second must also be a positive number to enable HTTP commands, and they will be limited by that amount. On a fresh instance both “http_handler” and “http_per_second” are set for you; change them only to move or disable the HTTP surface.
The HTTP surface is served under a dedicated /http/ path (so it can’t shadow the web portal’s own routes). When a request to http://<mush>/http/<path> arrives, SharpMUSH invisibly runs the HTTP Handler object (@config http_handler), executing an @include me/<method>. e.g: `@include me/get`.
Immediately when the @include finishes, the http request is complete. Any queued entries (such as @wait, $-commands, etc) are not going to be sent to the HTTP client - you’ll need to code using @include, /inline switches, and the like.
%0 will be the pathname with the /http prefix stripped — a request to /http/path/to?foo=bar arrives as %0 = “/path/to?foo=bar”. So %0 is ”/”, “/path/to”, “/foo?bar=baz”, etc.
%1 will be the body of the request. If it’s json, use json_query to deal with it. If it’s form-encoded, look at formdecode()
Anything sent to the HTTP Handler player during evaluation of this code is included in the body sent to the HTTP Client. There is a maximum size of BUFFER_LEN for the body of the response.
To modify the response headers, use the command @respond
> &GET *HTTPHandler=say Somebody tried to HTTP GET %0!
You will very likely want to set the http_handler option in your mush.cnf file to ensure it survives over reboots and is actively receiving events even during startup.
By default, SharpMUSH will respond with a 404 NOT FOUND. You will need to use @respond to control what is sent to the client.
HTTP connections to SharpMUSH are limited to BUFFER_LEN in header and body size.
Incoming headers will be set in Q-registers: %q contains a list of all headers by name. Individual headers will be set in %q<hdr.name>, prefixed with hdr. e.g: %q<hdr.host> to obtain the value to the Host: header. Or %q<hdr.Cookie> for Cookies.
Multiple header lines will be added to the same q-register name, but %r-delimited. So two “Cookie:” lines becomes %q with two %r-delimited lines.
HTTP Responses are limited to BUFFER_LEN in response size. Anything sent to the HTTPHandler player, whether it uses think or is @pemitted, is added to the response buffer.
Within the context of an HTTP Player connection, @respond is used to modify the headers sent back to the HTTP client.
If an attribute exists, Penn defaults to 200 OK, and Content-Type “text/plain”
@respond <code> <text> changes the 1st line sent to the client (200 OK)
@respond/type <text> replaces the current Content-Type header. (text/plain)
@respond/header <name>=<value> adds a new Header. This can’t be undone, as it’s appended to a buffer. So you can add multiple headers w/ same name.
@respond commands are not required to be run before any output is sent to the player. For Content-Length purposes, Penn buffers all output before the @include finishes.
If @respond is run outside of an HTTP Context, the enactor will see “(HTTP): …” for debugging, but it isn’t buffered for output as if it was an active http request.
Note: @respond/type is not syntactic sugar for `@respond/header Content-Type`. An HTTP @respond typically should only have one content-type, and @respond/type overrides it. Using @respond/header to add Content-Type will create a second header named Content-Type.
must be 3 digits, followed by a space, then printable ascii text
Total length must be < 40 characters
This will be prepended by HTTP/1.1 when sent back to the client
@respond/header <name>: <value>
must be printable ascii characters (No accents, no %r)
must be printable, but accents allowed (No %r)
@respond/type <ctype>
should be alphanumeric, +, ., /, -. HTTP/1.1 does allow for parameters (text/plain; content-encoding=…), so we don’t enforce anything at present except printability().
formdecode() is intended for use with the HTTP Handler. See [http] for more.
formdecode() converts form-encoded data, such as HTTP GET paths (after the ?) or the contents of POST with form-urlencoded data. It searches for the parameter named and returns with its decoded value.
If is not given, formdecode() returns a list of parameter names.
If there are multiple values, they will be separated by (default %b)
formdecode() requires libcurl (@http) to be enabled.
formq() decodes form-encoded data — an HTTP query string or a form-urlencoded body — and sets one Q-register per parameter, so HTTP handler softcode can read named parameters directly instead of calling formdecode() per field. This is a SharpMUSH extension; there is no PennMUSH equivalent.
Each parameter becomes the register (default prefix FORM.), so ?name=Joe is readable as %q<form.name>. Names are normalized the same way HTTP header registers are (uppercased; anything outside A-Z 0-9 _ . - becomes _).
Array parameters collapse into one %r-separated register, whichever way the client spells them: repeated names (like=a&like=b) and bracket arrays (like[]=a&like[]=b) both produce %q<form.like> containing a%rb — the same convention as duplicate HTTP headers in %q<hdr.>*. Bare tokens (?debug with no =) become registers with an empty value.
formq() returns the space-separated list of normalized parameter names (without the prefix), mirroring %q.
The default HTTP verb handlers (see [http examples]) call formq() on the query string for you, so route sub-attributes can read %q<form.>* immediately.
SharpMUSH seeds default verb attributes (&GET, &POST, &PUT, &DELETE, &PATCH, &HEAD) onto the http_handler (#8) at first startup. They are seeded once and never overwritten — edit them freely.
Each default verb attribute routes by URL path to a backtick-namespaced sub-attribute. (Paths below are as the handler sees them — i.e. the browser URL /http/api/users with the /http mount prefix already stripped.)
GET /api/users?name=Joe+Smith => @include me/GET`API`USERS=<body>
Before dispatching, the router sets:
%q — the path mapped to attribute form: leading slash and query stripped, remaining slashes become backticks (apiusers`)
%q — the formq()-decoded query parameter name list; each parameter is readable as %q<form.>*
The sub-attribute receives %0 = the raw request body. The body is left raw on purpose — check %q<hdr.content-type> and use formq() or json_query() on %0 as appropriate. The raw query string remains available as after(%0,?) only at the verb level; sub-attributes read the decoded %q<form.>* registers instead.
The router guards the dispatch with @assert: a request whose path maps to no sub-attribute — including the bare root / — answers 404 API NOT FOUND and stops. The seeded router for each verb is:
think setq(fields,formq(after(%0,?)))
@assert cand(t(setr(attrpath,edit(before(rest(%0,/),?),/,`))),hasattr(me,GET`%q<attrpath>))=@respond 404 API NOT FOUND
SharpMUSH also seeds these routed sub-attributes (used by the web portal; edit freely — seeded once, never overwritten):
GET /http/characters (&GETCHARACTERS) — the **roster**: a JSON array of listed players, {name, objid, created, category}, …. It says who *exists*, not who is connected — for that see /http/online. Built with json_array(iter(filter(me/FNCHARVIS, lsearch(all,type,player)), u(me/FNCHARROW,%i0), , %r), %r). categorycomes from&FNCHARCAT — by default flag-based, first match wins: Wizard (WIZARD flag), Royalty (ROYALTY flag), Guest (the Guest power); everyone else is blank. Who is listed at all comes from &FNCHARVIS(1 to list, 0 to hide) — the default hides theGuestcategory and thepackage_manager` principal, which is seeded as a real player (it owns softcode-package objects) but is nobody’s character. Both are MUSH-side policy: redefine them freely; the portal hard-codes nothing — it lists exactly what comes back, grouping by label (alphabetically) and pooling blanks in an untitled section at the bottom.
GET /http/online (&GETONLINE) — the **connection list**, same row shape as /http/characters. Built on lwho(), the same registry WHOreads, so an object that never binds a connection cannot appear here however it is flagged. Visibility comes from&FNONLINEVIS, which by default applies the &FNCHARVISrules plus hiding DARK players — notelwho()evaluatesCanSee()` against the caller, and the handler is wizard-flagged, so DARK players would otherwise be listed to anonymous web visitors. Redefine it to suit your game’s policy.
Both routes pass %r as the json_array() separator rather than taking the default. json_array() splits its input before parsing each element, and rows embed player names, which routinely contain spaces — with the default separator a name like Package Manager is shredded into fragments that are no longer valid JSON. Keep the separator to something your rows cannot contain if you rewrite these.
GET /http/profile/schema (&GETPROFILESCHEMA) — the profile field/section schema.
GET /http/profile?objid=#1:123 (&GETPROFILE) — one character's public profile. Characters are addressed by **objid** (stable across renames, safe against dbref recycling); an unknown objid answers 404 NO SUCH CHARACTER. Profile values live in PROFILE<key> attributes on the character.
These examples show the simple, direct-verb style: the whole &GET/&POST attribute answers the request itself. Note this replaces the seeded verb routers, so the examples set up their own dedicated handler to avoid clobbering the pre-populated #8 routers. For a real game you would usually keep the seeded routers on #8 and add routed sub-attributes instead (see [http routing]).
Examples all assume the following dedicated handler:
You can configure what paths and IPs you want to limit access to via @sitelock.
HTTP Requests will check @sitelock for IP restrictions and path restrictions for the config(http_handler) player. Right now, we don’t resolve hosts before HTTP connections are handled due to the time delay, but that may be an option in the future.
For path restrictions, @sitelock checks the pattern ”``”
Both IP and the “IP`Method`Path” approach check for “connect” option.