httpmsg
nimony/lib/std/http/httpmsg.nim
type HttpTag = enum tReq = (0, "tReq") tRes = (1, "tRes") tXhdr = (2, "tXhdr") tV10 = (3, "tV10") tV11 = (4, "tV11") tV20 = (5, "tV20") mGet = (6, "mGet") mHead = (7, "mHead") mPost = (8, "mPost") mPut = (9, "mPut") mDelete = (10, "mDelete") mConnect = (11, "mConnect") mOptions = (12, "mOptions") mTrace = (13, "mTrace") mPatch = (14, "mPatch") hAccept = (15, "hAccept") hAcceptCharset = (16, "hAcceptCharset") hAcceptEncoding = (17, "hAcceptEncoding") hAcceptLanguage = (18, "hAcceptLanguage") hAcceptRanges = (19, "hAcceptRanges") hAge = (20, "hAge") hAllow = (21, "hAllow") hAuthorization = (22, "hAuthorization") hCacheControl = (23, "hCacheControl") hConnection = (24, "hConnection") hContentDisposition = (25, "hContentDisposition") hContentEncoding = (26, "hContentEncoding") hContentLanguage = (27, "hContentLanguage") hContentLength = (28, "hContentLength") hContentLocation = (29, "hContentLocation") hContentRange = (30, "hContentRange") hContentType = (31, "hContentType") hCookie = (32, "hCookie") hDate = (33, "hDate") hEtag = (34, "hEtag") hExpect = (35, "hExpect") hExpires = (36, "hExpires") hForwarded = (37, "hForwarded") hFrom = (38, "hFrom") hHost = (39, "hHost") hIfMatch = (40, "hIfMatch") hIfModifiedSince = (41, "hIfModifiedSince") hIfNoneMatch = (42, "hIfNoneMatch") hIfRange = (43, "hIfRange") hIfUnmodifiedSince = (44, "hIfUnmodifiedSince") hLastModified = (45, "hLastModified") hLocation = (46, "hLocation") hMaxForwards = (47, "hMaxForwards") hOrigin = (48, "hOrigin") hPragma = (49, "hPragma") hProxyAuthenticate = (50, "hProxyAuthenticate") hProxyAuthorization = (51, "hProxyAuthorization") hRange = (52, "hRange") hReferer = (53, "hReferer") hRetryAfter = (54, "hRetryAfter") hServer = (55, "hServer") hSetCookie = (56, "hSetCookie") hTe = (57, "hTe") hTrailer = (58, "hTrailer") hTransferEncoding = (59, "hTransferEncoding") hUpgrade = (60, "hUpgrade") hUserAgent = (61, "hUserAgent") hVary = (62, "hVary") hVia = (63, "hVia") hWarning = (64, "hWarning") hWwwAuthenticate = (65, "hWwwAuthenticate") hXForwardedFor = (66, "hXForwardedFor") hXForwardedProto = (67, "hXForwardedProto") hXRequestId = (68, "hXRequestId") vKeepAlive = (69, "vKeepAlive") vClose = (70, "vClose") vChunked = (71, "vChunked") vGzip = (72, "vGzip") vDeflate = (73, "vDeflate") vIdentity = (74, "vIdentity") vTrailers = (75, "vTrailers") vNoCache = (76, "vNoCache") vNoStore = (77, "vNoStore")
func dollar`.HttpTag(e: HttpTag): stringconst vUpgrade: HttpTagConnection: upgrade. The same spelling names a header and one ofConnection's values, and a tag pool maps spellings to ids — so they are one tag, and the position in the tree says which role it is in.const MethodLow: HttpTagconst MethodHigh: HttpTagconst HeaderLow: HttpTagconst HeaderHigh: HttpTagconst ValueLow: HttpTagconst ValueHigh: HttpTagconst MaxHttpTags: int64One-token tag ids. See
HttpTagson why we stop here rather than spending a second token.const MaxTagNameLen: int64Longest spelling
lookupHeaderwill even consider. Every built-in tag is far shorter, so this only ever rejects something that was going to miss anyway — cheaply, and without touching the pool.type HttpTags = ref HttpTags.Obj not niltype HttpTags.Obj = object pool: ref TagPool.Obj byLen: seq lenStart: array[0..65, int32]
proc newHttpTags(): ref HttpTags.Obj not nilA fresh tag space, seeded with the built-in vocabulary. An application makes ONE during init and threads it: into every
HttpMsgit builds, into everyHttpConnit serves, and intoregisterHeaderfor the headers it indexes on. An id only means anything against the space it came from, so passing it is what makes "the same id everywhere" a property you can see in the signatures rather than one the process has to promise.template tag(e: HttpTag): TagIdThe pool id of a built-in tag. Ids are
ord + 1by construction, so this is arithmetic, not a lookup.proc isMethod(t: TagId): boolproc isKnownHeader(t: TagId): boolproc name(tags: ref HttpTags.Obj not nil; t: TagId): stringThe wire spelling of a registered tag;
""for an id nobody registered.template spelling(tags: ref HttpTags.Obj not nil; t: TagId): lent stringThe wire spelling, borrowed rather than copied — a template so it inlines to the table access. This is what a serializer wants;
namecopies.proc registerHeader(tags: ref HttpTags.Obj not nil; name: string): TagIdRegister a header the application indexes on, so it gets the same integer-compare treatment as
hHost. Call during init, before the first connection: a tag space is monotonic, so a name added later is one every message parsed against it so far reported as(xhdr …).Returns
TagId(0)when the tag space is full — a startup error, and the only reason this can fail.Idempotent: a name already registered comes back with the pool untouched, so several independent components (or several tests sharing one tag space) can each ask for the headers they need without having to agree on who goes first.
proc lookupHeader(tags: ref HttpTags.Obj not nil; name: openArray): TagIdWire bytes to a
TagId, folding ASCII case as HTTP requires. AnswersTagId(0)for a name nobody registered — those become(xhdr …).This never interns, and it never allocates. It is the one lookup the parser is allowed to perform on attacker-controlled bytes.
proc lookupValue(tags: ref HttpTags.Obj not nil; name: openArray): TagIdLike
lookupHeader, but answers only for spellings in the header-value range — soConnection: hostdoes not come back as thehosttag.proc lookupMethod(tags: ref HttpTags.Obj not nil; name: openArray): TagIdWire bytes to a method tag, case-sensitively as HTTP requires. Answers
TagId(0)for anything that is not one of the nine built-in methods.type HttpMsg = object buf: TokenBuf tags: ref HttpTags.Obj not nil live: bool
proc =wasMoved(m: var HttpMsg).nodestroy: the buffer has been handed to the destination, so clearing this one must not run its destructor. Without this hooklivewould survive the move and a moved-from message would claim to own a buffer it no longer has — which is exactly the question the event loop asks when it decides whether to reclaime.msg.proc hasBuf(m: HttpMsg): boolWhether this message owns a buffer. A moved-from message does not, which is how the event loop tells "the handler took it" from "it is still mine".
proc initHttpMsg(tags: ref HttpTags.Obj not nil; cap: int64): HttpMsgA message with its own literal pool, sharing
tags' tag space.proc reset(m: var HttpMsg)Drop the content, keeping the token buffer's allocation — which is the one that grew to fit the traffic and the reason recycling a message beats building a new one. The literal pool is replaced rather than emptied: it holds only the values too long to sit inline in a token, so it is typically small or untouched, and a fresh
Poolis one allocation against the certainty that no id from the old message stays reachable.proc startRequest(m: var HttpMsg; meth: TagId; target: string; v: TagId)Begin
(req (METHOD) target (version) …). Headers follow; callfinishwhen done.proc startResponse(m: var HttpMsg; status: int64; v: TagId)Begin
(res status (version) …).proc addHeader(m: var HttpMsg; h: TagId; value: string)A header whose value is a string.
proc addHeader(m: var HttpMsg; h: TagId; value: openArray)As above, from a byte view — what a parser has, without cutting a slice out of its read buffer to get it.
proc addHeader(m: var HttpMsg; h: TagId; value: int64)A header whose value is numeric —
Content-Lengthand friends. Stored as anIntLit, so it is parsed once here and never re-parsed on access.proc addHeader(m: var HttpMsg; h: TagId; values: openArray)A list-valued header as one node with several children — the shape a comma-separated header is meant to take, so that reading it back does not mean splitting a string again.
proc addHeader(m: var HttpMsg; h: TagId; value: TagId)A header whose value is itself drawn from a known vocabulary, e.g.
(connection (keep-alive)). Checking one is then an integer compare.proc addHeader(m: var HttpMsg; h: HttpTag; value: string)proc addHeader(m: var HttpMsg; h: HttpTag; value: int64)proc addHeader(m: var HttpMsg; h: HttpTag; value: HttpTag)proc addHeader(m: var HttpMsg; h: HttpTag; values: openArray)proc addHeader(m: var HttpMsg; h: HttpTag; value: openArray)The gap in the
HttpTagset: a value that is already bytes somewhere, which is how a header a server formats into a fixed buffer (Date) arrives. Without it such a caller has to spelltag(h)for this one header and not for its neighbours.proc addOtherHeader(m: var HttpMsg; name: string; value: string)A header nobody registered:
(xhdr "name" "value"). The application provably does not index this by constant — if it did, it would have registered it — so this form is never on a hot path.proc addOtherHeader(m: var HttpMsg; name: openArray; value: openArray)As above, from byte views.
proc startRequest(m: var HttpMsg; meth: TagId; target: openArray; v: TagId)As
startRequest, with the target still in the parser's buffer.proc finish(m: var HttpMsg)Close the message node, and take the buffer's cursor owner while
mis still mutable. Required before any accessor, and for two reasons now: the tree has to be closed to be walked, and every accessor below reads throughreadonlyCursorAt, which needs the buffer to be owned already — an ownerless cursor resolvesstrValagainst the fallback pool rather than this message's own.finishis the one point in a message's life that is both mutable and guaranteed to run before any read.The owner survives until the next mutation, which
prepareMutationuses to drop it again — so a recycled message re-takes it here, once.proc isRequest(m: HttpMsg): boolproc isResponse(m: HttpMsg): boolproc methodOf(m: HttpMsg): TagIdThe request's method tag, or
TagId(0)on a response.proc target(m: HttpMsg): stringThe request target, or
""on a response.proc statusOf(m: HttpMsg): int64The response status, or
0on a request.proc versionOf(m: HttpMsg): TagIdproc rootCursor(m: HttpMsg): CursorA cursor at the message node itself, for consumers that need the raw tree — a serializer reaching the target without copying it out, say.
iterator headers(m: HttpMsg): TagIdEvery header in wire order.
tag(tXhdr)is yielded for the unregistered ones; askotherHeadersfor their names.The
castis the cursor refcount, not the message: an iterator is inferred.noSideEffectand taking a reference on the buffer's owner is a write. Nothing aboutmchanges — seereadCursor.iterator headerNodes(m: HttpMsg): CursorA cursor at each header node, so a serializer can reach the raw values without them being copied out first.
subdescends into one. Seeheaderson the cast.iterator otherHeaders(m: HttpMsg): tuple[string, string]Name/value for the headers that have no tag. See
headerson the cast.proc contains(m: HttpMsg; h: TagId): boolWhether the message carries
hat least once.proc contains(m: HttpMsg; h: HttpTag): boolproc countHeader(m: HttpMsg; h: TagId): int64How many times
happears. More than once matters for the headers that frame a message: twoContent-Lengthlines are a request-smuggling vector, not a formatting quirk.proc countHeader(m: HttpMsg; h: HttpTag): int64proc getStr(m: HttpMsg; h: TagId): stringThe first value of
has a string, or""when absent. A value stored as a tag ((connection (keep-alive))) reads back as its wire spelling.proc getStr(m: HttpMsg; h: HttpTag): stringproc getInt(m: HttpMsg; h: TagId; default: int64): int64The first value of
has an integer — no re-parsing, it was stored as one.defaultwhen the header is absent or is not numeric.proc getInt(m: HttpMsg; h: HttpTag; default: int64): int64proc getTag(m: HttpMsg; h: TagId): TagIdThe first value of
hwhen it is itself a tag, elseTagId(0). This is the integer-compare path:m.getTag(hConnection) == tag(vKeepAlive).proc getTag(m: HttpMsg; h: HttpTag): TagIdproc contentLength(m: HttpMsg): int64-1when absent. One token read; the digits were parsed at parse time.proc isKeepAlive(m: HttpMsg): boolThe check every request pays for, as an integer compare rather than a case-insensitive string compare.