Lack Middleware Roundup
Background
Clack applications can be configured with a middleware chain, enabling modifications to the request or the response at each link in the chain. This architecture enables the extraction of common functionalities like request parsing, access logging, and error handling from application-specific or server-specific logic, promoting code reuse and a clearer separation of concerns.
Clack is directly inspired by Ruby’s Rack, but the general approach of a server abstraction interface and use of application middleware has been applied in a number of programming languages, including: Python (WSGI, ASGI), Clojure (Ring), Perl (Plack), Java (Servlet), and Elixir (Plug).
Official middlewares
The lack repository on GitHub contains the official middlewares for Clack applications.
accesslog
Symbol: LACK/MIDDLEWARE/ACCESSLOG:*LACK-MIDDLEWARE-ACCESSLOG*
The accesslog middleware logs each request. By default the middleware prints to *STANDARD-OUTPUT*
, but this can be overridden by defining keyword parameter LOGGER
, a function. The logger function takes a single parameter representing the output string returned by the formatter.
The default formatter may be overridden by providing the FORMATTER
keyword parameter, a function. The formatter function takes three parameters: the environment plist, the response, and optionally the current time, a LOCAL-TIME:TIMESTAMP
.
auth-basic
Symbol: LACK/MIDDLEWARE/AUTH/BASIC:*LACK-MIDDLEWARE-AUTH-BASIC*
The auth-basic middleware implements the Basic Authentication protocol, which proceeds as follows: when the client sends a request to access a protected resource, the server responds with a 401 response status code along with the “www-authenticate” header to indicate that the endpoint requires Basic Authentication (example response header: “www-authenticate: Basic realm=example-realm”), which causes the browser to display a dialogue for username and password input. When the user submits the dialogue, the browser submits another request and includes the Authorization header that includes the base64 encoded username and password, like: “Authorization: Basic
The AUTHENTICATOR
keyword parameter is required for the auth-basic middleware, which must be a function that takes two parameters: the user identifier and the password. The authenticator function returns one or two values, the first representing whether the operation was successful, and the second optional value the user; on success, the middleware sets the REMOTE-USER
key in the Clack environment, setting the value to the either the second returned value (when present), or else the username.
The optional keyword parameter REALM
should be a string, and defaults to “restricted area”. This parameter represents the realm returned in the “www-authenticate” response header.
backtrace
Symbol: LACK/MIDDLEWARE/BACKTRACE:*LACK-MIDDLEWARE-BACKTRACE*
The backtrace middleware wraps the Clack app in a HANDLER-BIND
statement and defines a single handler for ERROR
conditions, meaning that the handler will run when any error is signalled. By default, the printed error is logged to *ERROR-OUTPUT*
, but this can be overridden by defining optional keyword parameter OUTPUT
. Optional keyword parameter RESULT-ON-ERROR
is NIL by default and may be assigned to either a function or a Clack response list; when a list is given and an error is signalled, the middleware returns this list as the response instead of calling the next middleware.
csrf
Symbol: LACK/MIDDLEWARE/CSRF:*LACK-MIDDLEWARE-CSRF*
CSRF middleware is an implementation of the synchronizer token pattern used to protect against cross-site request forgery attacks. It requires the session hash-table associated with environment key LACK.SESSION
to be set, meaning the app first needs to be wrapped with the Lack session middleware before being wrapped with CSRF middleware.
When the request is made with an unsafe HTTP method (POST, PUT, PATCH, DELETE), the middleware finds the synchronizer token stored in the session hash under the key specified by keyword parameter SESSION-KEY
, which defaults to “csrftoken”, and when found, compares the value of the session token to the CSRF parameter from the request body. Keyword parameter SESSION-TOKEN
determines the parameter in the request body that contains the submitted token, which also defaults to “csrftoken”.
When the tokens strings match, the middleware proceeds to the next middleware, otherwise returning a 400 response. The error response can be customized by setting optional keyword parameter BLOCK-APP
, a function that takes the Clack environment and returns a Clack response list. When provided with a non-NIL value, optional keyword parameter ONE-TIME
causes the token stored in the session to be deleted after a successful comparison.
The package also exports functions CSRF-TOKEN
and CSRF-HTML-TAG
. Call CSRF-TOKEN
to retrieve the synchronizer token from the session. Call CSRF-HTML-TAG
to return a hidden form input HTML string that contains the synchronizer token as its value; the hidden field should be included in every form submission handled by the application.
dbpool
Symbol: LACK/MIDDLEWARE/DBPOOL:*LACK-MIDDLEWARE-DBPOOL*
The dbpool middleware depends on libraries anypool, which provides a generic approach to connection pooling based on user-provided callbacks, and cl-dbi, which provides an interface for accessing a DBMS (SQLite3, MySQL, and PostgreSQL are supported). The dbpool middleware requires providing a symbol representing the database as an ordinal parameter. Keyword parameter CONNECT-ARGS
is also required and is a plist passed as the arguments to function DBI:CONNECT
. Optional keyword POOL-ARGS
is also a plist, and is passed to function ANYPOOL:MAKE-POOL
, allowing for pool customization with options MAX-OPEN-COUNT
, MAX-IDLE-COUNT
, TIMEOUT
, and IDLE-TIMEOUT
.
The package also exports macro WITH-CONNECTION
, that you can use in your route handlers to checkout a connection from the specified pool. The macro takes a two-item list followed by the body. The list contains first a name for the variable for which to bind to the connection, and second the database symbol established in the middleware configuration. A HANDLER-BIND
is defined with a handler for ANYPOOL:TOO-MANY-OPEN-CONNECTION
: when this error is signalled, a 503 Clack response is returned instead of calling the next middleware.
;; NOTE: This assumes that maindb was specified as the database-id (the
;; ordinal parameter) in the middleware configuration.
(with-connection (conn 'maindb)
(format t "Connected to database.~%")
;; Code to perform database operations using the connection "conn"
)
mount
Symbol: LACK/MIDDLEWARE/MOUNT:*LACK-MIDDLEWARE-MOUNT*
The mount middleware allows for mounting another Clack application at a specific URL path prefix within the parent application, specified by ordinal parameter PATH
. The next ordinal parameter, MOUNT-APP
, represents the application to mount; it can be either a standard function that takes the Clack environment as its only parameter, or a CLOS instance that inherits from LACK/COMPONENT:LACK-COMPONENT
.
When the middleware processes the request, it compares the current path (from the PATH-INFO
environment key) to the prefix specified by PATH
. On an exact match, the middleware updates PATH-INFO
to “/” and calls the mounted application; when the current path otherwise starts with the prefix identified by PATH
, it extracts the remaining path information, updates PATH-INFO
to the extracted remainder, and and calls the mounted application. If the path does not match the mounted application’s path prefix, the middleware calls the parent application as normal.
session
Symbol: LACK/MIDDLEWARE/SESSION:*LACK-MIDDLEWARE-SESSION*
The session store adds a hash-table under the LACK.SESSION
environment key for storing session data; the middleware also adds LACK.SESSION.OPTIONS
to the environment, a list which may be updated to control session behavior.
Keyword parameter STORE
signifies which backend to use, defaulting to the memory store. Alternative store implementations include cl-redis and cl-dbi based stores. Keyword parameter STATE
signifies the state management mechanism, defaulting to cookies, and this is the only implementation defined.
Example: set a cookie named “_sid” with session state stored in process memory.
;; NOTE: the store parameter here is optional and reflects the default value
(funcall lack/middleware/session:*lack-middleware-session*
*app*
:store (lack/session/store/memory:make-memory-store)
:state (lack/session/state/cookie:make-cookie-state
:cookie-key "_sid"
:path "/"
:domain "localhost"
:expires (+ 600 (get-universal-time))
:httponly t
:secure nil
:samesite :strict))
static
Symbol: LACK/MIDDLEWARE/STATIC:*LACK-MIDDLEWARE-STATIC*
Static middleware serves static files from disk and is intended primarily for use in development; in production, it is recommended to serve static files from a CDN or a server optimized for serving files such as NGINX. The middleware takes two keyword parameters, both of which are optional.
Keyword parameter ROOT
, which defaults to the current directory, represents the location of the static files on disk to be served by the application; set it to a pathname if your static files are located outside of the current directory.
Optional keyword parameter PATH
may be either NIL, a string, or a function that returns a string or NIL. When PATH
is provided as a string and the request path starts with the specified string, the middleware updates the PATH-INFO
key in the environment and serves the static file. When PATH
is specified as a function, it takes takes a single parameter representing the current path; when the function returns a (string) value, the middleware updates the PATH-INFO
key in the environment calls the CALL-APP-FILE
function to serve the file.
Lack Builder
Lack Builder can be used to effortlessly compose a middleware pipeline, avoiding cumbersome nested FUNCALL
expressions.
The final argument to macro LACK:BUILDER
is the Clack application to be wrapped, and every other argument is either a keyword or a list representing the middleware to place in the chain. Lists allow passing arguments to the middleware function.
Lack uses a naming convention to look up the middleware function for each specified middleware item. If the pipeline were to include keyword EXAMPLE
, Lack would search for the following symbol: LACK/MIDDLEWARE/EXAMPLE:*LACK-MIDDLEWARE-EXAMPLE*
.
(lack:builder
(:session :state (lack/session/state/cookie:make-cookie-state
:cookie-key "_sid"
:path "/"
:domain "localhost"
:httponly t
:secure nil
:samesite :strict))
:csrf
(:backtrace :result-on-error `(500
(:content-type "text/plain"
:content-length 21)
("Internal Server Error")))
*app*)