From bdf74e29855c818f5a042a786ecb3b644578d7dd Mon Sep 17 00:00:00 2001 From: Lucian Mogosanu Date: Fri, 30 Aug 2019 18:26:02 +0300 Subject: [PATCH] posts: 09d --- drafts/000-hunchentoot-vib.markdown | 469 ------------------------------- posts/y06/09d-hunchentoot-vib.markdown | 475 ++++++++++++++++++++++++++++++++ 2 files changed, 475 insertions(+), 469 deletions(-) delete mode 100644 drafts/000-hunchentoot-vib.markdown create mode 100644 posts/y06/09d-hunchentoot-vib.markdown diff --git a/drafts/000-hunchentoot-vib.markdown b/drafts/000-hunchentoot-vib.markdown deleted file mode 100644 index 4f20374..0000000 --- a/drafts/000-hunchentoot-vib.markdown +++ /dev/null @@ -1,469 +0,0 @@ ---- -postid: 000 -title: Hunchentoot: requests and replies [b] -date: August 30, 2019 -author: Lucian Mogoșanu -tags: tech, tmsr ---- - -This post is part of a series on [Common Lisp WWWism][cl-www], more -specifically a continuation of "[Hunchentoot: requests and -replies][hunchentoot-via]". In this second part of "requests and -replies" we will dissect "replies" and what remains of "requests", in -precisely the reverse order. - -We ended [last time][hunchentoot-via] with a look at code that parses -GET and POST requests. This time we will look (in the following order) -at: a set of "interface methods" aimed to be employed by the user when -implementing request handlers; the [process-request](#pr) method; and, -last but not least, at the methods exposed by the "reply" component in -this orchestra. - -First, we notice that most request accessors come with wrapper methods -whose names end in a "star", which, by some arbitrary convention, -means that they implicitly bind their current "request" parameter to -the special variable \*request\*[^1]. Let's take a look at a few of -these: - -[sns] [**script-name\***][ht-sns] - -[qss] [**query-string\***][ht-qss] - -[gps] [**get-parameters\***][ht-gps] - -[his] [**headers-in\***][ht-his] - -[cis] [**cookies-in\***][ht-cis] - -[ras] [**remote-addr\***][ht-ras] - -[rps] [**remote-port\***][ht-rps] - -[las] [**local-addr\***][ht-las] - -[lps] [**local-port\***][ht-lps] - -[rus] [**request-uri\***][ht-rus] - -[rms] [**request-method\***][ht-rms] - -[sps] [**server-protocol\***][ht-sps] - -The implementation of these functions is, as can be readily observed, -trivial, and the meaning should be easily deducible from the name; -thus I won't bother the reader with redundant details. However, -there's also: - -[pps] [**post-parameters\***][ht-pps]: -This one calls [post-parameters](#pp), which: - -[pp] [**post-parameters**][ht-pp]: Running -":before" more specific implementations of this method are called; -given a request, it calls [maybe-read-post-parameters][ht-mrpp]. If -the post-parameters [slot][ht-pp-slot] is nil, then set the force -parameter to true. - -Similarly: - -[rrp] -[**recompute-request-parameters**][ht-rrp]: Calls -[maybe-read-post-parameters][ht-mrpp] with the force argument always -set to true. Also, set the [get-parameters][ht-gp-slot] to the -[re-parsed][ht-fuelta] value of [query-string][ht-qs-slot]. - -However, we also have: - -[hi] [**header-in**][ht-hi]: Gets a -specific header from the [headers-in][ht-hi-slot] slot. - -[his2] [**header-in\***][ht-his2]: The -same as [header-in](#hi), with the request parameter bound to -\*request\*. - -And there's also these methods which dynamically bind \*request\*, -although their name doesn't end in "star"[^2]: - -[a] [**authorization**][ht-a]: Reads the -"authorization" header and tries to parse the user/password -combination associated included, if they exist. Notice the magic -number 5 used in this shitty piece of coad, because writing a proper -parser for yet-another-set-of-ad-hoc-encoded-parameters is too much -work. - -[rra] [**real-remote-addr**][ht-rra]: -Reads the "X-Forwarded-For" header and, if it exists, it parses the -client and proxy address fields and returns them. Apparently there's -no "official spec" to be found for this, apparently the whole thing's -superseded by some other syntax specified in [some -RFC][rfc-7239]. Hunchentoot doesn't parse this one, however, -so... well! - -[h] [**host**][ht-h]: Reads the "Host" -header. - -[ua] [**user-agent**][ht-ua]: Reads the -"User-Agent" header. - -[ci] [**cookie-in**][ht-ci]: Looks up the -cookie with the given name in the [cookies][ht-ci-slot] slot. - -[r] [**referer**][ht-r]: Reads the "Referer" -header. - -[gp] [**get-parameter**][ht-gp]: Looks up -a specific GET parameter in [get-parameters][ht-gp-slot]. - -[pp2] [**post-parameter**][ht-pp2]: -Looks up a specific POST parameter in the -[post-parameters][ht-pp-slot] slot. - -[p] [**parameter**][ht-p]: Looks up a GET -parameter, and, if not found, a POST parameter with a specific name. - -[hims] [**handle-if-modified-since**][ht-hims]: Reads -the "If-Modified-Since" header and compares it with a given time -argument. If the two dates match, then a. set the reply as follows: -a1. the content-length to nil; a2. remove the content-length header; -a3. the return-code to http-not-modified; then -b. [abort-request-handler][ht-arh]. - -[rpd] [**raw-post-data**][ht-rpd]: -a\. First, try to set an external-format based on the values of -arguments force-binary, external-format and force-text; if no -external-format is set and force-binary is not set, then try getting -one from [external-format-from-content-type][ht-effct], and if that -fails, fall back to \*hunchentoot-default-external-format\*. - -b\. Get the [raw-post-data][ht-rpd-slot] slot; if not set, then get -post data by calling [get-post-data][ht-gpd]. Given a local binding to -raw-post-data, the return value is determined based, in order, on the -following conditions: b1. if raw-post-data is a stream, then return -it; else b2. if raw-post-data is t or nil, return nil; else b3. if -external-format was set, [octets-to-string][flex-ots] on raw-post-data -and return the result; otherwise b4. return raw-post-data as-is. - -[arv] [**aux-request-value**][ht-arv]: -If the given request is non-nil, then look up a value in the alist -given by the [aux-data][ht-ad-slot] slot. Return, as multiple values, -both the value and the symbol-value pair itself. - -There's also a [defsetf][ht-arv2] defined for this function, which -looks up the value and a. if it exists, sets the new value, and b. if -it doesn't, pushes a new symbol-value pair to [aux-data][ht-ad-slot]. - -[darv] [**delete-aux-request-value**][ht-darv]: -Similarly to [aux-request-value](#arv), operates on -[aux-data][ht-ad-slot]; this one deletes a symbol-value binding from -the alist. - -[rp] [**request-pathname**][ht-rp]: Given -a request, a. get its [script-name][ht-sn-slot]; and -b. [parse-path](#pp3). If drop-prefix is set to a string representing -a path prefix, then the pathname is returned sans said prefix. - -[pp3] [**parse-path**][ht-pp3]: -Sanitization function used by [request-pathname](#rp) to bring a -path-string to the proper abstraction level, i.e. a Common Lisp -pathname. After a. parsing the namestring using either -[parse-namestring][clhs-parse-namestring] or some -implementation-specific function, e.g. SBCL's -[parse-native-namestring][sbcl-nf], run the following checks: - -b\. the host field of the pathname is nil, or it equals that of -\*default-pathname-defaults\*; c\. the device field of the pathname is -nil, or it equals that of \*default-pathname-defaults\*; d\. the -directory component is nil, or it's a relative pathname without :up -and :wild; e\. the pathname name and type fields are either nil or -strings; f\. the namestring isn't "..". - -g\. When (b)-(f) are satisfied, return the parsed pathname. - -Since all these functions bind at least one of their arguments to -\*request\*, we also have: - -[wrp] [**within-request-p**][ht-wrp]: -When \*request\* is bound, return it. - -And finally: - -[pr] [**process-request**][ht-pr]: This -function is on the main request processing/handling path, and is -called by [process-connection][ht-pc] whenever a new HTTP request is -available. It does a bunch of more or less related things, let's take -them one by one. - -a\. the first part of the function a1. defines and binds some special -variables, e.g. the current \*request\* being processed, and a2. wraps -everything into some condition handling code, namely: a2i. it "maps" -all [conditions][ht-wmc][^3]; a2ii. wraps this in an -[unwind-protect][clhs-unwind-protect][^4]; a2iii. all this wrapped in -a [catch][clhs-catch][^5]. - -b\. the code at (a) wraps a b1. local procedure definition, -report-error-to-client, which [logs][ht-lms] an error and returns a -http-internal-error, i.e. [start-output][ht-so] on the result of an -[acceptor-status-message][ht-asm]; and b2. a call to -[handle-request][ht-hr] wrapped in a catch for "handler-done"[^6], -with the return values for handle-request bound to "contents", "error" -and "backtrace"; b3. if "error", then report-error-to-client; b4. if -headers are not yet sent, [start-output][ht-so] with the return code -of \*reply\* and whatever contents we have; b4. if no contents are -set, then get a default page from [acceptor-status-message][ht-asm]; -b5. if an error occurs during (b4), then call report-error-to-client. - -c\. this occurs on the "cleanup-form" part of the unwind-protect at -(a2ii): if there are any temporary files that were set up during the -function, delete them. - -This function is all over the fucking place, owing mainly to the -pretense of "modularity" and separation between requests and replies --- I did say they're part of the same logical unit, didn't I? Just -look at it: "request processing" calls "start output" -- which by the -way, is part of "headers", right? but this same "start output" is the -one which actually delivers a response to the client, which response -delivery oughta be part of "reply"!! So to conclude: this whole shit -is in dire need of refactoring and ultimately a complete rewrite. No, -no, this time I'm pretty damn sure it's not the protocol's fault for -this abomination. - -Since we've fortunately exhausted request.lisp, we're left with: - -**[[]]** [**reply**][ht-c-rep]: The "response" counterpart to request -objects; holds the response [content-type][ht-ct-slot], -[length][ht-cl-slot], [headers][ht-ho-slot], [return -code][ht-rc-slot], [encoding][ht-ef-slot] and -[cookies][ht-co-slot]. Implemented methods: - -[ii3] [**initialize-instance**][ht-ii3]: -":after" the reply object is instantiated, set content-type header to -the \*default-content-type\*. - -The following are, similarly to the request accessor functions, -wrappers which implicitly bind their reply argument to the \*reply\* -special. Also, most of them also come with a setf definition, which -simply set the associated slot of the reply argument (also bound to -\*reply\* unless specified) to a given new-value. I shan't bother to -give details where there are none, here they are: - -[hos] [**headers-out\***][ht-hos] - -[cos] [**cookies-out\***][ht-cos]: -[setf-er][ht-cos-setf]. - -[cts] [**content-type\***][ht-cts]: -[setf-er][ht-cts-setf]. - -[cls] [**content-length\***][ht-cls]: -[setf-er][ht-cls-setf]. - -[rcs] [**return-code\***][ht-rcs]: -[setf-er][ht-rcs-setf]. - -[refs] [**reply-external-format\***][ht-refs]: -[setf-er][ht-refs-setf]. - -[hosp] [**header-out-set-p**][ht-hosp]: Looks up an -output [header][ht-ho-slot] and returns true if found. - -[ho] [**header-out**][ht-ho]: Looks up a(n -output) [header][ht-ho-slot] and, if found, returns its value. Unlike -[the previous](#hosp) function, this one also comes with a -[setf-er][ht-ho-setf] which e.g. performs conversions if the name -provided is a string etc. and checks the types of content-type and -content-length headers. - -[co] [**cookie-out**][ht-co]: Looks up a -cookie in the respective [alist][ht-co-slot] and returns its value if -found. - -This is it, then: a complete review of all the [core][hunchentoot-iii] -architectural components of Hunchentoot. Remember, this program -actually works, and not by mere happenstance, but quite deliberately, -as the result of what I expect was hard work to bake this tangled -shawarma. Note that I don't plan to add *any* functionality to the -current mess, at least not until completely rewriting it, preferably -on top of a not-completely-fucked [WWW stack][logs-mp-on-lisp-www]. If -you want to make a Lisp logger/blog/web front-end using current-day -WWW, then it seems unfortunately that this is the best you got. - -Having said that, the next episode in the Hunchentoot saga will reveal -two items: a genesis of the Hunchentoot that was reviewed here, and -one of Hunchentoot plus [all its dependencies][hunchentoot-deps], for -which I can only vouch by noting that they sorta work, not that they -do precisely what's written on the label. Then I can start building -around this: a comment mechanism for this blog, an IRC logger, a -pastebin and all those other fundamental webthings that keep things -running. - -[^1]: Special variables have a [special meaning][cltl-special] in - Common Lisp -- see what I did there? Namely, a special variable - may be declared globally and "bound dynamically", i.e. its value - may depend on its current execution context, such as, say, the - function or the thread where execution takes place, if it's been - bound this way using e.g. a let -- the naive me, who started his - adventure in Lisp programming in Scheme, thought let can only be - used for lexical bindings, and yet... look! Now, *if* there is no - such "dynamic binding" for that variable, then the variable's - *global* binding is used, which binding is shared between - execution contexts. - - Yeah, what can I say... I didn't write this language, okay? - -[^2]: Not like this so-called convention is specified anywhere, so - we're stuck guessing why these are named this way while the others - are named the other. I could come up with some ideas, but why - bother. - -[^3]: with-mapped-conditions is actually a [usocket][usocket-wmc] - macro which, annoyingly enough, is not documented. Long story - short, it can be used to define a context in which all usocket - conditions are safely handled. (TODO: add reference to usocket - coad?) - -[^4]: Which ensures that the code at the end of the function (TODO add - reference) gets executed no matter what conditions occur during - execution. - -[^5]: More precisely, "request-processed" [throw][clhs-throw]s are - caught here, which, as the comment describes, are thrown by - [start-output][ht-so] after responding to a HEAD - request. The idea is, when the request method is HEAD, exit - process-request once the headers are sent, bypassing all the other - request handling code. - - Since we're here, let's examine [start-output][ht-so]: this is the - function that pushes the actual response headers and content to - the socket. a\. determine whether the keep-alive should be set: if - either: a1. we used chunked transmission; or a2. the request - method is HEAD; or a3. the return code is http-not-modified; or - a4. the content-length or the content are set; then set - keep-alive-p to true. b\. when the acceptor requests output - chunking, set the Transfer-Encoding header to chunked; c\. if - c1. keep-alive-p is set to true, then set - \*finish-processing-socket\* to nil; if a keep-alive was - requested, then set the Connection header to Keep-Alive, and set - the keep-alive timeout; otherwise, if c2. the Connection header is - not set, set it to Close. d\. set the Server header, if the - acceptor contains one and it wasn't already set. e\. set the Date - header; f\. do some weird-ass URL rewriting for - sessions\*. g\. convert the content to the encoding given by - [reply-external-format\*](#refs). h\. set the - content-length. i\. if \*headers-sent\* was set, then return; - otherwise j\. set \*headers-sent\* to true and call - [send-response][ht-sr], then k\. throw a request-processed to end - processing. Otherwise, l\. make our stream into a "chunked - stream". - - From the sausage above, [send-response][ht-sr] is what does the - actual pushing of the first response line, headers, content and so - on. - - The only thing this whole load of crap gives is the ability for - the user to do his own processing, by writing a custom - start-output which throws its own request-processed and bypasses - the entire process-request control flow. This is, as far as I can - tell, evidence of serious brain rot on the part of whoever wrote - this, since the user can write his own thing by simply modifying - the existing code, otherwise there being really no need for all - this imagined flexibility. - - \-\-\- - \*: I don't know what the fuck this is, make sure to keep note of - it if you ever plan to use the "sessions" thing. - -[^6]: We get there via [abort-request-handler][ht-arh], basically. - -[ht-pr]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L219 -[ht-wrp]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L262 -[ht-rrp]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L334 -[ht-sns]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L344 -[ht-qss]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L349 -[ht-gps]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L354 -[ht-pp]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L359 -[ht-pps]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L368 -[ht-his]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L373 -[ht-cis]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L378 -[ht-hi]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L383 -[ht-his2]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L389 -[ht-a]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L394 -[ht-ras]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L407 -[ht-rps]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L411 -[ht-las]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L415 -[ht-lps]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L419 -[ht-rra]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L423 -[ht-h]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L433 -[ht-rus]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L437 -[ht-rms]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L441 -[ht-sps]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L445 -[ht-ua]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L449 -[ht-ci]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L453 -[ht-r]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L458 -[ht-gp]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L462 -[ht-pp2]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L467 -[ht-p]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L472 -[ht-hims]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L480 -[ht-rpd]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L508 -[ht-arv]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L551 -[ht-arv2]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L559 -[ht-darv]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L576 -[ht-pp3]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L585 -[ht-rp]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L613 -[ht-mrpp]: /posts/y06/09c-hunchentoot-via.html#mrpp -[ht-pc]: http://thetarpit.org/posts/y06/098-hunchentoot-iv.html#pc -[ht-wmc]: http://coad.thetarpit.org/hunchentoot/c-util.lisp.html#L353 -[ht-so]: http://coad.thetarpit.org/hunchentoot/c-headers.lisp.html#L67 -[ht-lms]: /posts/y06/098-hunchentoot-iv.html#lms -[ht-asm]: /posts/y06/098-hunchentoot-iv.html#asm -[ht-hr]: /posts/y06/098-hunchentoot-iv.html#hr -[ht-pp-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L75 -[ht-gp-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L71 -[ht-qs-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L83 -[ht-fuelta]: /posts/y06/09c-hunchentoot-via.html#fn5 -[ht-hi-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L36 -[ht-ci-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L68 -[ht-arh]: /posts/y06/098-hunchentoot-iv.html#selection-762.0-762.5 -[ht-rpd-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L94 -[ht-gpd]: /posts/y06/09c-hunchentoot-via.html#gpd -[ht-effct]: /posts/y06/09c-hunchentoot-via.html#effct -[ht-ad-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L90 -[ht-sn-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L79 -[ht-sr]: http://coad.thetarpit.org/hunchentoot/c-headers.lisp.html#L160 -[ht-c-rep]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L31 -[ht-ct-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L32 -[ht-cl-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L35 -[ht-ho-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L40 -[ht-rc-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L46 -[ht-ef-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L51 -[ht-co-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L55 -[ht-ii3]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L69 -[ht-hos]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L72 -[ht-cos]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L77 -[ht-cos-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L82 -[ht-cts]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L87 -[ht-cts-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L91 -[ht-cls]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L95 -[ht-cls-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L99 -[ht-rcs]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L103 -[ht-rcs-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L108 -[ht-refs]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L112 -[ht-refs-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L116 -[ht-hosp]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L120 -[ht-ho]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L125 -[ht-co]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L130 -[ht-ho-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L135 -[clhs-parse-namestring]: http://clhs.lisp.se/Body/f_pars_1.htm -[sbcl-nf]: http://www.sbcl.org/manual/#Native-Filenames -[flex-ots]: http://edicl.github.io/flexi-streams/#octets-to-string -[cltl-special]: https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node58.html -[rfc-7239]: https://tools.ietf.org/html/rfc7239#section-4 -[usocket-wmc]: https://quickref.common-lisp.net/usocket.html#go-to-the-USOCKET_003ccolon_003e_003ccolon_003eWITH_002dMAPPED_002dCONDITIONS-macro -[clhs-catch]: http://www.lispworks.com/documentation/HyperSpec/Body/s_catch.htm -[clhs-throw]: http://clhs.lisp.se/Body/s_throw.htm -[clhs-unwind-protect]: http://www.lispworks.com/documentation/HyperSpec/Body/s_unwind.htm -[hunchentoot-iii]: /posts/y06/097-hunchentoot-iii.html -[logs-mp-on-lisp-www]: http://logs.nosuchlabs.com/log/trilema/2019-08-01#1926066 -[hunchentoot-deps]: /posts/y06/097-hunchentoot-iii.html#fn3 diff --git a/posts/y06/09d-hunchentoot-vib.markdown b/posts/y06/09d-hunchentoot-vib.markdown new file mode 100644 index 0000000..1e6638a --- /dev/null +++ b/posts/y06/09d-hunchentoot-vib.markdown @@ -0,0 +1,475 @@ +--- +postid: 09d +title: Hunchentoot: requests and replies [b] +date: August 30, 2019 +author: Lucian Mogoșanu +tags: tech, tmsr +--- + +This post is part of a series on [Common Lisp WWWism][cl-www], more +specifically a continuation of "[Hunchentoot: requests and +replies][hunchentoot-via]". In this second part of "requests and +replies" we will dissect "replies" and what remains of "requests", in +precisely the reverse order. + +We ended [last time][hunchentoot-via] with a look at code that parses +GET and POST requests. This time we will look, in the following order, +at: a set of "interface methods" aimed to be employed by the user when +implementing request handlers; the [process-request](#pr) method; and, +last but not least, the methods exposed by the "reply" component. + +First, we notice that most request accessors come with wrapper methods +whose names end in a "star", which, by some arbitrary convention, +means that they implicitly bind their current "request" parameter to +the special variable \*request\*[^1]. Let's take a look at a few of +these: + +[sns] [**script-name\***][ht-sns] + +[qss] [**query-string\***][ht-qss] + +[gps] [**get-parameters\***][ht-gps] + +[his] [**headers-in\***][ht-his] + +[cis] [**cookies-in\***][ht-cis] + +[ras] [**remote-addr\***][ht-ras] + +[rps] [**remote-port\***][ht-rps] + +[las] [**local-addr\***][ht-las] + +[lps] [**local-port\***][ht-lps] + +[rus] [**request-uri\***][ht-rus] + +[rms] [**request-method\***][ht-rms] + +[sps] [**server-protocol\***][ht-sps] + +The implementation of these functions is, as can be readily observed, +trivial, and the meaning should be easily deducible from the name; +thus I won't bother the reader with redundant details. However, +there's also: + +[pps] [**post-parameters\***][ht-pps]: +This one calls [post-parameters](#pp), which: + +[pp] [**post-parameters**][ht-pp]: Runs +":before" more specific implementations of this method are called; +given a request, it calls [maybe-read-post-parameters][ht-mrpp]. If +the post-parameters [slot][ht-pp-slot] is nil, then set the force +parameter to true -- in other words, "force" +maybe-read-post-parameters to populate that slot with the parsed POST +parameters if they don't already exist. + +Similarly: + +[rrp] +[**recompute-request-parameters**][ht-rrp]: Calls +[maybe-read-post-parameters][ht-mrpp] with the force argument always +set to true. Also, set the [get-parameters][ht-gp-slot] to the +[re-parsed][ht-fuelta] value of [query-string][ht-qs-slot]. + +However, we also have: + +[hi] [**header-in**][ht-hi]: Gets a +specific header from the [headers-in][ht-hi-slot] slot. + +[his2] [**header-in\***][ht-his2]: The +same as [header-in](#hi), with the request parameter bound to +\*request\*. + +And there's also these methods which dynamically bind \*request\*, +although their name doesn't end in "star"[^2]: + +[a] [**authorization**][ht-a]: Reads the +"authorization" header and tries to parse the user/password +combination associated included, if they exist. Notice the magic +number 5 used in this shitty piece of coad, because writing a proper +parser for yet-another-set-of-ad-hoc-encoded-parameters is too much +work. + +[rra] [**real-remote-addr**][ht-rra]: +Reads the "X-Forwarded-For" header and, if it exists, it parses the +client and proxy address fields and returns them. Apparently there's +no "official spec" to be found for this, apparently the whole thing's +superseded by some other syntax specified in [some +RFC][rfc-7239]. Hunchentoot doesn't parse this other one, however, +so... well! + +[h] [**host**][ht-h]: Reads the "Host" +header. + +[ua] [**user-agent**][ht-ua]: Reads the +"User-Agent" header. + +[ci] [**cookie-in**][ht-ci]: Looks up the +cookie with the given name in the [cookies][ht-ci-slot] slot. + +[r] [**referer**][ht-r]: Reads the "Referer" +header. + +[gp] [**get-parameter**][ht-gp]: Looks up +a specific GET parameter in [get-parameters][ht-gp-slot]. + +[pp2] [**post-parameter**][ht-pp2]: +Looks up a specific POST parameter in the +[post-parameters][ht-pp-slot] slot. + +[p] [**parameter**][ht-p]: Looks up a GET +parameter, and, if not found, a POST parameter with a specific name. + +[hims] [**handle-if-modified-since**][ht-hims]: Reads +the "If-Modified-Since" header and compares it with a given time +argument. If the two dates match, then a. set the reply as follows: +a1. the content-length to nil; a2. remove the content-length header; +a3. the return-code to http-not-modified; then +b. [abort-request-handler][ht-arh]. + +[rpd] [**raw-post-data**][ht-rpd]: +a\. First, try to set an external-format based on the values of +arguments force-binary, external-format and force-text; if no +external-format is set and force-binary is not set, then try getting +one from [external-format-from-content-type][ht-effct], and if that +fails, fall back to \*hunchentoot-default-external-format\*. + +b\. Get the [raw-post-data][ht-rpd-slot] slot; if not set, then get +post data by calling [get-post-data][ht-gpd]. Given a local binding to +raw-post-data, the return value is determined based, in order, on the +following conditions: b1. if raw-post-data is a stream, then return +it; else b2. if raw-post-data is t or nil, return nil; else b3. if +external-format was set, [octets-to-string][flex-ots] on raw-post-data +and return the result; otherwise b4. return raw-post-data as-is. + +[arv] [**aux-request-value**][ht-arv]: +If the given request is non-nil, then look up a value in the alist +given by the [aux-data][ht-ad-slot] slot. Return, as multiple values, +both the value and the symbol-value pair itself. + +There's also a [defsetf][ht-arv2] defined for this function, which +looks up the value and a. if it exists, sets the new value, and b. if +it doesn't, pushes a new symbol-value pair to [aux-data][ht-ad-slot]. + +[darv] [**delete-aux-request-value**][ht-darv]: +Similarly to [aux-request-value](#arv), operates on +[aux-data][ht-ad-slot]; this one deletes a symbol-value binding from +the alist. + +[rp] [**request-pathname**][ht-rp]: Given +a request, a. get its [script-name][ht-sn-slot]; and +b. [parse-path](#pp3). If drop-prefix is set to a string representing +a path prefix, then the pathname is returned sans said prefix. + +[pp3] [**parse-path**][ht-pp3]: +Sanitization function used by [request-pathname](#rp) to bring a +path-string to the proper abstraction level, i.e. a Common Lisp +pathname. After a. parsing the namestring using either +[parse-namestring][clhs-parse-namestring] or some +implementation-specific function, e.g. SBCL's +[parse-native-namestring][sbcl-nf], run the following checks: + +b\. the host field of the pathname is nil, or it equals that of +\*default-pathname-defaults\*; c\. the device field of the pathname is +nil, or it equals that of \*default-pathname-defaults\*; d\. the +directory component is nil, or it's a relative pathname without :up +and :wild; e\. the pathname name and type fields are either nil or +strings; f\. the namestring isn't "..". + +g\. When (b)-(f) are satisfied, return the parsed pathname. + +Since all these functions bind at least one of their arguments to +\*request\*, we also have: + +[wrp] [**within-request-p**][ht-wrp]: +When \*request\* is bound, return it. + +And finally: + +[pr] [**process-request**][ht-pr]: This +function is on the main request processing/handling path, and is +called by [process-connection][ht-pc] whenever a new HTTP request is +available. It does a bunch of more or less related things, let's take +them one by one. + +a\. the first part of the function a1. defines and binds some special +variables, e.g. the current \*request\* being processed, and a2. wraps +everything into some condition handling code, namely: a2i. it "maps" +all [conditions][ht-wmc][^3]; a2ii. wraps this in an +[unwind-protect][clhs-unwind-protect][^4]; a2iii. all this wrapped in +a [catch][clhs-catch][^5]. + +b\. the code at (a) wraps a b1. local procedure definition, +report-error-to-client, which [logs][ht-lms] an error and returns a +http-internal-error, i.e. [start-output][ht-so] on the result of an +[acceptor-status-message][ht-asm]; and b2. a call to +[handle-request][ht-hr] wrapped in a catch for "handler-done"[^6], +with the return values for handle-request bound to "contents", "error" +and "backtrace"; b3. if "error", then report-error-to-client; b4. if +headers are not yet sent, [start-output][ht-so] with the return code +of \*reply\* and whatever contents we have; b4. if no contents are +set, then get a default page from [acceptor-status-message][ht-asm]; +b5. if an error occurs during (b4), then call report-error-to-client. + +c\. this occurs on the "cleanup-form" part of the unwind-protect at +(a2ii): if there are any temporary files that were set up during the +function, delete them. + +This function is all over the fucking place, owing mainly to the +pretense of "modularity" and separation between requests and replies +-- I did say they're part of the same logical unit, didn't I? Just +look at it: "request processing" calls "start output" -- which by the +way, is part of "headers", right? but this same "start output" is the +one which actually delivers a response to the client, which response +delivery oughta be part of "reply"!! So to conclude: this whole shit +is in dire need of refactoring and ultimately a complete rewrite. No, +no, this time I'm pretty damn sure it's not the protocol's fault for +this abomination. + +Now that we've exhausted request.lisp, we're left with: + +**[[]]** [**reply**][ht-c-rep]: The "response" counterpart to request +objects; holds the response [content-type][ht-ct-slot], +[length][ht-cl-slot], [headers][ht-ho-slot], [return +code][ht-rc-slot], [encoding][ht-ef-slot] and +[cookies][ht-co-slot]. Implemented methods: + +[ii3] [**initialize-instance**][ht-ii3]: +":after" the reply object is instantiated, set content-type header to +the \*default-content-type\*. + +The following are, similarly to the request accessor functions, +wrappers which implicitly bind their reply argument to the \*reply\* +special. Also, most of them also come with a setf definition, which +simply set the associated slot of the reply argument (also bound to +\*reply\* unless specified) to a given new-value. I shan't bother to +give details where there are none, here they are: + +[hos] [**headers-out\***][ht-hos] + +[cos] [**cookies-out\***][ht-cos]: +[setf-er][ht-cos-setf]. + +[cts] [**content-type\***][ht-cts]: +[setf-er][ht-cts-setf]. + +[cls] [**content-length\***][ht-cls]: +[setf-er][ht-cls-setf]. + +[rcs] [**return-code\***][ht-rcs]: +[setf-er][ht-rcs-setf]. + +[refs] [**reply-external-format\***][ht-refs]: +[setf-er][ht-refs-setf]. + +[hosp] [**header-out-set-p**][ht-hosp]: Looks up an +output [header][ht-ho-slot] and returns true if found. + +[ho] [**header-out**][ht-ho]: Looks up a(n +output) [header][ht-ho-slot] and, if found, returns its value. Unlike +[the previous](#hosp) function, this one also comes with a +[setf-er][ht-ho-setf] which e.g. performs conversions if the name +provided is a string etc. and checks the types of content-type and +content-length headers. + +[co] [**cookie-out**][ht-co]: Looks up a +cookie in the respective [alist][ht-co-slot] and returns its value if +found. + +This is it, then: a complete review of all the [core][hunchentoot-iii] +architectural components of Hunchentoot. Remember, this program +actually works, and not by mere happenstance, but quite deliberately, +as the result of what I expect was hard work to bake this tangled +shawarma. Note that I don't plan to add *any* functionality to the +current mess, at least not until refactoring/rewriting/bulldozing it, +preferably on top of a not-completely-fucked [WWW +stack][logs-mp-on-lisp-www]. If you want to make a Lisp +logger/blog/web front-end using current-day WWW, then it seems +unfortunately that this is the best you got. + +Having said that, the next episode in the Hunchentoot saga will reveal +two items: a genesis of the Hunchentoot that was reviewed here, and +one of Hunchentoot plus [all its dependencies][hunchentoot-deps], for +which I can only vouch by noting that they sorta work, not that they +do precisely what's written on the label. Then I can start building +around this: a comment mechanism for this blog, an IRC logger, a +pastebin and all those other fundamental webthings that keep things +running. + +[^1]: Special variables have a [special meaning][cltl-special] in + Common Lisp -- see what I did there? Namely, a special variable + may be declared globally and "bound dynamically", i.e. its value + may depend on its current execution context, such as, say, the + function or the thread where execution takes place, if it's been + bound this way using e.g. a let -- the naive me, who started his + adventure in Lisp programming in Scheme, thought let can only be + used for lexical bindings, and yet... look! Now, *if* there is no + such "dynamic binding" for that variable, then the variable's + *global* binding is used, which binding is shared between + execution contexts. + + Yeah, what can I say... I didn't write this language, okay? + +[^2]: Not like this so-called convention is specified anywhere, so + we're stuck guessing why these are named this way while the others + are named the other. I could come up with some ideas, but why + bother. + +[^3]: with-mapped-conditions is actually a [usocket][usocket-wmc] + macro which, annoyingly enough, is not documented. Long story + short, it can be used to define a context in which all usocket + conditions are safely handled. I guess I'll get to this when + usocket is to be either sanitized or ripped altogether out of + Hunchentoot. + +[^4]: Which ensures that the code [at the end][ht-pr-unwind] of the + function gets executed no matter what conditions occur during + execution. + +[^5]: More precisely, "request-processed" [throw][clhs-throw]s are + caught here, which, as the comment describes, are thrown by + [start-output][ht-so] after responding to a HEAD + request. The idea is, when the request method is HEAD, exit + process-request once the headers are sent, bypassing all the other + request handling code. + + Since we're here, let's examine [start-output][ht-so]: this is the + function that pushes the actual response headers and content to + the socket. a\. determine whether the keep-alive should be set: if + either: a1. we used chunked transmission; or a2. the request + method is HEAD; or a3. the return code is http-not-modified; or + a4. the content-length or the content are set; then set + keep-alive-p to true. b\. when the acceptor requests output + chunking, set the Transfer-Encoding header to chunked; c\. if + c1. keep-alive-p is set to true, then set + \*finish-processing-socket\* to nil; if a keep-alive was + requested, then set the Connection header to Keep-Alive, and set + the keep-alive timeout; otherwise, if c2. the Connection header is + not set, set it to Close. d\. set the Server header, if the + acceptor contains one and it wasn't already set. e\. set the Date + header; f\. do some weird-ass URL rewriting for + sessions\*. g\. convert the content to the encoding given by + [reply-external-format\*](#refs). h\. set the + content-length. i\. if \*headers-sent\* was set, then return; + otherwise j\. set \*headers-sent\* to true and call + [send-response][ht-sr], then k\. throw a request-processed to end + processing. Otherwise, l\. make our stream into a "chunked + stream". + + From the sausage above, [send-response][ht-sr] is what does the + actual pushing of the first response line, headers, content and so + on. + + The only thing this whole load of crap gives is the ability for + the user to do his own processing, by writing a custom + start-output which throws its own request-processed and bypasses + the entire process-request control flow. This is, as far as I can + tell, evidence of serious brain rot on the part of whoever wrote + this, since the user can write his own thing by simply modifying + the existing code, otherwise there being really no need for all + this imagined flexibility. + + \-\-\- + \*: I don't know what the fuck this is, make sure to keep note of + it if you ever plan to use the "sessions" thing. + +[^6]: We get there via [abort-request-handler][ht-arh], basically. + +[ht-pr]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L219 +[ht-wrp]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L262 +[ht-rrp]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L334 +[ht-sns]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L344 +[ht-qss]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L349 +[ht-gps]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L354 +[ht-pp]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L359 +[ht-pps]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L368 +[ht-his]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L373 +[ht-cis]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L378 +[ht-hi]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L383 +[ht-his2]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L389 +[ht-a]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L394 +[ht-ras]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L407 +[ht-rps]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L411 +[ht-las]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L415 +[ht-lps]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L419 +[ht-rra]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L423 +[ht-h]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L433 +[ht-rus]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L437 +[ht-rms]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L441 +[ht-sps]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L445 +[ht-ua]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L449 +[ht-ci]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L453 +[ht-r]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L458 +[ht-gp]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L462 +[ht-pp2]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L467 +[ht-p]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L472 +[ht-hims]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L480 +[ht-rpd]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L508 +[ht-arv]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L551 +[ht-arv2]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L559 +[ht-darv]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L576 +[ht-pp3]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L585 +[ht-rp]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L613 +[ht-mrpp]: /posts/y06/09c-hunchentoot-via.html#mrpp +[ht-pc]: http://thetarpit.org/posts/y06/098-hunchentoot-iv.html#pc +[ht-wmc]: http://coad.thetarpit.org/hunchentoot/c-util.lisp.html#L353 +[ht-so]: http://coad.thetarpit.org/hunchentoot/c-headers.lisp.html#L67 +[ht-lms]: /posts/y06/098-hunchentoot-iv.html#lms +[ht-asm]: /posts/y06/098-hunchentoot-iv.html#asm +[ht-hr]: /posts/y06/098-hunchentoot-iv.html#hr +[ht-pp-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L75 +[ht-gp-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L71 +[ht-qs-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L83 +[ht-fuelta]: /posts/y06/09c-hunchentoot-via.html#fn5 +[ht-hi-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L36 +[ht-ci-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L68 +[ht-arh]: /posts/y06/098-hunchentoot-iv.html#selection-762.0-762.5 +[ht-rpd-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L94 +[ht-gpd]: /posts/y06/09c-hunchentoot-via.html#gpd +[ht-effct]: /posts/y06/09c-hunchentoot-via.html#effct +[ht-ad-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L90 +[ht-sn-slot]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L79 +[ht-pr-unwind]: http://coad.thetarpit.org/hunchentoot/c-request.lisp.html#L255 +[ht-sr]: http://coad.thetarpit.org/hunchentoot/c-headers.lisp.html#L160 +[ht-c-rep]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L31 +[ht-ct-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L32 +[ht-cl-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L35 +[ht-ho-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L40 +[ht-rc-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L46 +[ht-ef-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L51 +[ht-co-slot]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L55 +[ht-ii3]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L69 +[ht-hos]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L72 +[ht-cos]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L77 +[ht-cos-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L82 +[ht-cts]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L87 +[ht-cts-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L91 +[ht-cls]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L95 +[ht-cls-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L99 +[ht-rcs]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L103 +[ht-rcs-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L108 +[ht-refs]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L112 +[ht-refs-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L116 +[ht-hosp]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L120 +[ht-ho]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L125 +[ht-co]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L130 +[ht-ho-setf]: http://coad.thetarpit.org/hunchentoot/c-reply.lisp.html#L135 +[cl-www]: /posts/y05/090-tmsr-work-ii.html#selection-108.0-108.17 +[hunchentoot-via]: /posts/y06/09c-hunchentoot-via.html +[clhs-parse-namestring]: http://clhs.lisp.se/Body/f_pars_1.htm +[sbcl-nf]: http://www.sbcl.org/manual/#Native-Filenames +[flex-ots]: http://edicl.github.io/flexi-streams/#octets-to-string +[cltl-special]: https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node58.html +[rfc-7239]: https://tools.ietf.org/html/rfc7239#section-4 +[usocket-wmc]: https://quickref.common-lisp.net/usocket.html#go-to-the-USOCKET_003ccolon_003e_003ccolon_003eWITH_002dMAPPED_002dCONDITIONS-macro +[clhs-catch]: http://www.lispworks.com/documentation/HyperSpec/Body/s_catch.htm +[clhs-throw]: http://clhs.lisp.se/Body/s_throw.htm +[clhs-unwind-protect]: http://www.lispworks.com/documentation/HyperSpec/Body/s_unwind.htm +[hunchentoot-iii]: /posts/y06/097-hunchentoot-iii.html +[logs-mp-on-lisp-www]: http://logs.nosuchlabs.com/log/trilema/2019-08-01#1926066 +[hunchentoot-deps]: /posts/y06/097-hunchentoot-iii.html#fn3 -- 1.7.10.4