Lwan is a high-performance & scalable web server.
The project web site contains more details.
You can either build Lwan yourself, use a container image, or grab a package from your favorite distribution.
Before installing Lwan, ensure all dependencies are installed. All of them are common dependencies found in any GNU/Linux distribution; package names will be different, but it shouldn't be difficult to search using whatever package management tool that's used by your distribution.
The build system will look for these libraries and enable/link if available.
-DENABLE_BROTLI=NO
-DENABLE_ZSTD=NO
-DENABLE_TLS=ON
(default) is passed:
-DUSE_ALTERNATIVE_MALLOC
to CMake with the following values:
pacman -S cmake zlib
pkg install cmake pkgconf
apt-get update && apt-get install git cmake zlib1g-dev pkg-config
brew install cmake
pacman -S cmake zlib sqlite luajit mariadb-libs gperftools valgrind mbedtls
pkg install cmake pkgconf sqlite3 lua51
apt-get update && apt-get install git cmake zlib1g-dev pkg-config lua5.1-dev libsqlite3-dev libmariadb-dev libmbedtls-dev
brew install cmake mariadb-connector-c sqlite lua@5.1 pkg-config
~$ git clone git://github.com/lpereira/lwan
~$ cd lwan
Create the build directory
~/lwan$ mkdir build
~/lwan$ cd build
Selecting a release version (no debugging symbols, messages, enable some optimizations, etc):
~/lwan/build$ cmake .. -DCMAKE_BUILD_TYPE=Release
If you'd like to enable optimizations but still use a debugger, use this instead:
~/lwan/build$ cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo
To disable optimizations and build a more debugging-friendly version:
~/lwan/build$ cmake .. -DCMAKE_BUILD_TYPE=Debug
This will generate a few binaries:
src/bin/lwan/lwan
: The main Lwan executable. May be executed with --help
for guidance.src/bin/testrunner/testrunner
: Contains code to execute the test suite (src/scripts/testsuite.py
).src/samples/freegeoip/freegeoip
: FreeGeoIP sample implementation. Requires SQLite.src/samples/techempower/techempower
: Code for the TechEmpower Web Framework benchmark. Requires SQLite and MariaDB libraries.src/samples/clock/clock
: Clock sample. Generates a never-ending animated GIF file that always shows the local time.src/samples/forthsalon/forthsalon
: Generates a never-ending animated GIF from a program written in the Forth Salon dialect of the Forth programming language. In construction!src/samples/forthsalon/forth
: Test harness for the Forth dialect used in the forthsalon
sample.src/bin/tools/mimegen
: Builds the extension-MIME type table. Used during the build process.src/bin/tools/bin2hex
: Generates a C file from a binary file, suitable for use with #include. Used during the build process.src/bin/tools/configdump
: Dumps a configuration file using the configuration reader API. Used for testing.src/bin/tools/weighttp
: Rewrite of the weighttp
HTTP benchmarking tool.src/bin/tools/statuslookupgen
: Generates a perfect hash table for HTTP status codes and their descriptions. Used during the build process.Passing -DCMAKE_BUILD_TYPE=Release
will enable some compiler optimizations (such as LTO) and tune the code for current architecture.
Important
Please use the release build when benchmarking. The default is the Debug build, which not only logs all requests to the standard output, but does so while holding a lock, severely holding down the server.
The default build (i.e. not passing -DCMAKE_BUILD_TYPE=Release
) will build a version suitable for debugging purposes. This version can be used under Valgrind (if its headers are present) and includes debugging messages that are stripped in the release version. Debugging messages are printed for each and every request.
On these builds, sanitizers can be enabled. To select which one to build Lwan with, specify one of the following options to the CMake invocation line:
-DSANITIZER=ubsan
selects the Undefined Behavior Sanitizer.-DSANITIZER=address
selects the Address Sanitizer.-DSANITIZER=thread
selects the Thread Sanitizer.Alternative memory allocators can be selected as well. Lwan currently supports TCMalloc, mimalloc, and jemalloc out of the box. To use either one of them, pass -DALTERNATIVE_MALLOC=name
to the CMake invocation line, using the names provided in the "Optional dependencies" section.
The -DUSE_SYSLOG=ON
option can be passed to CMake to also log to the system log in addition to the standard output.
If you're building Lwan for a distribution, it might be wise to use the -DMTUNE_NATIVE=OFF
option, otherwise the generated binary may fail to run on some computers.
TLS support is enabled automatically in the presence of a suitable mbedTLS installation on Linux systems with headers new enough to support kTLS, but can be disabled by passing -DENABLE_TLS=NO
to CMake.
~/lwan/build$ make testsuite
This will compile the testrunner
program and execute regression test suite in src/scripts/testsuite.py
.
~/lwan/build$ make benchmark
This will compile testrunner
and execute benchmark script src/scripts/benchmark.py
.
Lwan can also be built with the Coverage build type by specifying -DCMAKE_BUILD_TYPE=Coverage
. This enables the generate-coverage
make target, which will run testrunner
to prepare a test coverage report with lcov.
Every commit in this repository triggers the generation of this report, and results are publicly available.
Set up the server by editing the provided lwan.conf
; the format is explained in details below.
Note
Lwan will try to find a configuration file based in the executable name in the current directory; testrunner.conf
will be used for the testrunner
binary, lwan.conf
for the lwan
binary, and so on.
Configuration files are loaded from the current directory. If no changes are made to this file, running Lwan will serve static files located in the ./wwwroot
directory. Lwan will listen on port 8080 on all interfaces.
Lwan will detect the number of CPUs, will increase the maximum number of open file descriptors and generally try its best to autodetect reasonable settings for the environment it's running on. Many of these settings can be tweaked in the configuration file, but it's usually a good idea to not mess with them.
Tip
Optionally, the lwan
binary can be used for one-shot static file serving without any configuration file. Run it with --help
for help on that.
Lwan uses a familiar key = value
configuration file syntax. Comments are supported with the #
character (similar to e.g. shell scripts, Python, and Perl). Nested sections can be created with curly brackets. Sections can be empty; in this case, curly brackets are optional.
some_key_name
is equivalent to some key name
in configuration files (as an implementation detail, code reading configuration options will only be given the version with underscores).
Tip
Values can contain environment variables. Use the syntax ${VARIABLE_NAME}
. Default values can be specified with a colon (e.g. ${VARIABLE_NAME:foo}
, which evaluates to ${VARIABLE_NAME}
if it's set, or foo
otherwise).
sound volume = 11 # This one is 1 louder
playlist metal {
files = '''
/multi/line/strings/are/supported.mp3
/anything/inside/these/are/stored/verbatim.mp3
'''
}
playlist chiptune {
files = """
/if/it/starts/with/single/quotes/it/ends/with/single/quotes.mod
/but/it/can/use/double/quotes.s3m
"""
}
Some examples can be found in lwan.conf
and techempower.conf
.
Constants can be defined and reused throughout the configuration file by specifying them in a constants
section anywhere in the configuration file. A constant will be available only after that section defines a particular constant. Constants can be re-defined. If a constant isn't defined, its value will be obtained from an environment variable. If it's not defined in either one constants
section, or in the environment, Lwan will abort with an appropriate error message.
constants {
user_name = ${USER}
home_directory = ${HOME}
buffer_size = 1000000
}
The same syntax for default values specified above is valid here (e.g. specifying user_name
to be ${USER:nobody}
will set ${user_name}
to nobody
if ${USER}
isn't set in the environment variable or isn't another constant.)
str
Any kind of free-form text, usually application specific int
Integer number. Range is application specific time
Time interval. See table below for units bool
Boolean value. See table below for valid values
Time fields can be specified using multipliers. Multiple can be specified, they're just added together; for instance, "1M 1w" specifies "1 month and 1 week" (37 days). The following table lists all known multipliers:
Multiplier Descriptions
Seconds m
Minutes h
Hours d
Days w
7-day Weeks M
30-day Months y
365-day Years
Note
A number with a multiplier not in this table is ignored; a warning is issued while reading the configuration file. No spaces must exist between the number and its multiplier.
True Values False Values Any integer number different than 0 0on
off
true
false
yes
no
It's generally a good idea to let Lwan decide the best settings for your environment. However, not every environment is the same, and not all uses can be decided automatically, so some configuration options are provided.
Option Type Default Descriptionkeep_alive_timeout
time
15
Timeout to keep a connection alive quiet
bool
false
Set to true to not print any debugging messages. Only effective in release builds. expires
time
1M 1w
Value of the "Expires" header. Default is 1 month and 1 week threads
int
0
Number of I/O threads. Default (0) is the number of online CPUs proxy_protocol
bool
false
Enables the PROXY protocol. Versions 1 and 2 are supported. Only enable this setting if using Lwan behind a proxy, and the proxy supports this protocol; otherwise, this allows anybody to spoof origin IP addresses max_post_data_size
int
40960
Sets the maximum number of data size for POST requests, in bytes max_put_data_size
int
40960
Sets the maximum number of data size for PUT requests, in bytes max_file_descriptors
int
524288
Maximum number of file descriptors. Needs to be at least 10x threads
request_buffer_size
int
4096
Request buffer size length. If larger than the default of 4096
, it'll be dynamically allocated. allow_temp_files
str
""
Use temporary files; set to post
for POST requests, put
for PUT requests, or all
(equivalent to setting to post put
) for both. error_template
str
Default error template Template for error codes. See variables below. Variables for error_template
Variable Type Description short_message
str
Short error message (e.g. Not found
) long_message
str
Long error message (e.g. The requested resource could not be found on this server
)
Lwan can drop its privileges to a user in the system, and limit its filesystem view with a chroot. While not bulletproof, this provides a first layer of security in the case there's a bug in Lwan.
In order to use this feature, declare a straitjacket
(or straightjacket
) section, and set some options. This requires Lwan to be executed as root
.
Although this section can be written anywhere in the file (as long as it is a top level declaration), if any directories are open, due to e.g. instantiating the serve_files
module, Lwan will refuse to start. (This check is only performed on Linux as a safeguard for malconfiguration.)
Tip
Declare a Straitjacket right before a site
section in such a way that configuration files and private data (e.g. TLS keys) are out of reach of the server after initialization has taken place.
user
str
NULL
Drop privileges to this user name chroot
str
NULL
Path to chroot()
drop_capabilities
bool
true
Drop all capabilities with capset(2) (under Linux), or pledge(2) (under OpenBSD).
If there's a need to specify custom headers for each response, one can declare a headers
section in the global scope. The order which this section appears isn't important.
For example, this declaration:
headers {
Server = Apache/1.0.0 or nginx/1.0.0 (at your option)
Some-Custom-Header = ${WITH_THIS_ENVIRONMENT_VARIABLE}
}
Will both override the Server
header (Server: lwan
won't be sent), and set Some-Custom-Header
with the value obtained from the environment variable $WITH_THIS_ENVIRONMENT_VARIABLE
.
Some headers can't be overridden, as that would cause issues when sending their actual values while servicing requests. These include but is not limited to:
Date
Expires
WWW-Authenticate
Connection
Content-Type
Transfer-Encoding
Access-Control-Allow-
headersNote
Header names are also case-insensitive (and case-preserving). Overriding SeRVeR
will override the Server
header, but send it the way it was written in the configuration file.
Only two listeners are supported per Lwan process: the HTTP listener (listener
section), and the HTTPS listener (tls_listener
section). Only one listener of each type is allowed.
Warning
TLS support is experimental. Although it is stable during initial testing, your mileage may vary. Only TLSv1.2 is supported at this point, but TLSv1.3 is planned.
Note
TLS support requires 🐧 Linux with the tls.ko
module built-in or loaded. Support for other operating systems may be added in the future. FreeBSD seems possible, other operating systems do not seem to offer similar feature. For unsupported operating systems, using a TLS terminator proxy such as Hitch is a good option.
For both listener
and tls_listener
sections, the only parameter is the the interface address and port to listen on. The listener syntax is ${ADDRESS}:${PORT}
, where ${ADDRESS}
can either be *
(binding to all interfaces), an IPv6 address (if surrounded by square brackets), an IPv4 address, or a hostname. For instance, listener localhost:9876
would listen only in the lo
interface, port 9876
.
While a listener
section takes no keys, a tls_listener
section requires two: cert
and key
(each pointing, respectively, to the location on disk where the TLS certificate and private key files are located) and takes an optional boolean hsts
key, which controls if Strict-Transport-Security
headers will be sent on HTTPS responses.
Tip
To generate these keys for testing purposes, the OpenSSL command-line tool can be used like the following: openssl req -nodes -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 7
Note
It's recommended that a Straitjacket with a chroot
option is declared right after a tls_listener
section, in such a way that the paths to the certificate and key are out of reach from that point on.
If systemd socket activation is used, systemd
can be specified as a parameter. (If multiple listeners from systemd are specified, systemd:FileDescriptorName
can be specified, where FileDescriptorName
follows the conventions set in the systemd.socket
documentation.)
Examples:
listener *:8080 # Listen on all interfaces, port 8080, HTTP
tls_listener *:8081 { # Listen on all interfaces, port 8081, HTTPS
cert = /path/to/cert.pem
key = /path/to/key.pem
}
# Use named systemd socket activation for HTTP listener
listener systemd:my-service-http.socket
# Use named systemd socket activation for HTTPS listener
tls_listener systemd:my-service-https.socket {
...
}
A site
section groups instances of modules and handlers that will respond to requests to a given URL prefix.
In order to route URLs, Lwan matches the largest common prefix from the request URI with a set of prefixes specified in the listener section. How a request to a particular prefix will be handled depends on which handler or module has been declared in the listener section. Handlers and modules are similar internally; handlers are merely functions and hold no state, and modules holds state ("instance"). Multiple instances of a module can appear in a listener section.
There is no special syntax to attach a prefix to a handler or module; all the configuration parser rules apply here. Use ${NAME} ${PREFIX}
to link the ${PREFIX}
prefix path to either a handler named ${NAME}
(if ${NAME}
begins with &
, as with C's "address of" operator), or a module named ${NAME}
. Empty sections can be used here.
Each module will have its specific set of options, and they're listed in the next sections. In addition to configuration options, a special authorization
section can be present in the declaration of a module instance. Handlers do not take any configuration options, but may include the authorization
section.
Tip
Executing Lwan with the --version
command-line argument will show a list of built-in modules and handlers.
The following is some basic documentation for the modules shipped with Lwan.
The serve_files
module will serve static files, and automatically create directory indices or serve pre-compressed files. It'll generally try its best to serve files in the fastest way possible according to some heuristics.
path
str
NULL
Path to a directory containing files to be served index_path
str
index.html
File name to serve as an index for a directory serve_precompressed_path
bool
true
If $FILE.gz exists, is smaller and newer than $FILE, and the client accepts gzip
encoding, transfer it auto_index
bool
true
Generate a directory list automatically if no index_path
file present. Otherwise, yields 404 auto_index_readme
bool
true
Includes the contents of README files as part of the automatically generated directory index directory_list_template
str
NULL
Path to a Mustache template for the directory list; by default, use an internal template read_ahead
int
131702
Maximum amount of bytes to read ahead when caching open files. A value of 0
disables readahead. Readahead is performed by a low priority thread to not block the I/O threads while file extents are being read from the filesystem. cache_for
time
5s
Time to keep file metadata (size, compressed contents, open file descriptor, etc.) in cache
Note
Files smaller than 16KiB will be compressed in RAM for the duration specified in the cache_for
setting. Lwan will always try to compress with deflate, and will optionally compress with Brotli and zstd (if Lwan has been built with proper support).
In cases where compression wouldn't be worth the effort (e.g. adding the Content-Encoding
header would result in a larger response than sending the uncompressed file, usually the case for very small files), Lwan won't spend time compressing a file.
For files larger than 16KiB, Lwan will not attempt to compress them. In future versions, it might do this and send responses using chunked-encoding while the file is being compressed (up to a certain limit, of course), but for now, only precompressed files (see serve_precompressed_path
setting in the table above) are considered.
For all cases, Lwan might try using the gzipped version if that's found in the filesystem and the client requested this encoding.
Variables fordirectory_list_template
Variable Type Description rel_path
str
Path relative to the root directory real path readme
str
Contents of first readme file found (readme
, readme.txt
, read.me
, README.TXT
, README
) file_list
iterator Iterates on file list file_list.zebra_class
str
odd
for odd items, or even
or even items file_list.icon
str
Path to the icon for the file type file_list.name
str
File name (escaped) file_list.type
str
File type (directory or regular file) file_list.size
int
File size file_list.unit
str
Unit for file_size
The lua
module will allow requests to be serviced by scripts written in the Lua programming language. Although the functionality provided by this module is quite spartan, it's able to run frameworks such as Sailor.
Scripts can be served from files or embedded in the configuration file, and the results of loading them, the standard Lua modules, and (optionally, if using LuaJIT) optimizing the code will be cached for a while.
Option Type Default Descriptiondefault_type
str
text/plain
Default MIME-Type for responses script_file
str
NULL
Path to Lua script cache_period
time
15s
Time to keep Lua state loaded in memory script
str
NULL
Inline lua script
Note
Lua scripts can't use global variables, as they may be not only serviced by different threads, but the state will be available only for the amount of time specified in the cache_period
configuration option. This is because each I/O thread in Lwan will create an instance of a Lua VM (i.e. one lua_State
struct for every I/O thread), and each Lwan coroutine will spawn a Lua thread (with lua_newthread()
) per request.
There's no need to have one instance of the Lua module for each endpoint; a single script, embedded in the configuration file or otherwise, can service many different endpoints. Scripts are supposed to implement functions with the following signature: handle_${METHOD}_${ENDPOINT}(req)
, where ${METHOD}
can be a HTTP method (i.e. get
, post
, head
, etc.), and ${ENDPOINT}
is the desired endpoint to be handled by that function. A generic handle(req)
function will be called if the specific version doesn't exist.
Tip
Use the root
endpoint for a catchall. For example, the handler function handle_get_root()
will be called if no other handler could be found for that request. If no catchall is specified, the server will return a 404 Not Found
error.
The req
parameter points to a metatable that contains methods to obtain information from the request, or to set the response, as seen below:
req:query_param(param)
returns the query parameter (from the query string) with the key param
, or nil
if not foundreq:post_param(param)
returns the post parameter (only for ${POST}
handlers) with the key param
, or nil
if not foundreq:set_response(str)
sets the response to the string str
req:say(str)
sends a response chunk (using chunked encoding in HTTP)req:send_event(event, str)
sends an event (using server-sent events)req:cookie(param)
returns the cookie named param
, or nil
is not foundreq:set_headers(tbl)
sets the response headers from the table tbl
; a header may be specified multiple times by using a table, rather than a string, in the table value ({'foo'={'bar', 'baz'}}
); must be called before sending any response with say()
or send_event()
req:header(name)
obtains the header from the request with the given name or nil
if not foundreq:sleep(ms)
pauses the current handler for the specified amount of millisecondsreq:ws_upgrade()
returns 1
if the connection could be upgraded to a WebSocket; 0
otherwisereq:ws_write_text(str)
sends str
through the WebSocket-upgraded connection as text framereq:ws_write_binary(str)
sends str
through the WebSocket-upgraded connection as binary framereq:ws_write(str)
sends str
through the WebSocket-upgraded connection as text or binary frame, depending on content containing only ASCII characters or notreq:ws_read()
returns a string with the contents of the last WebSocket frame, or a number indicating an status (ENOTCONN/107 on Linux if it has been disconnected; EAGAIN/11 on Linux if nothing was available; ENOMSG/42 on Linux otherwise). The return value here might change in the future for something more Lua-like.req:remote_address()
returns a string with the remote IP address.req:path()
returns a string with the request path.req:query_string()
returns a string with the query string (empty string if no query string present).req:body()
returns the request body (POST/PUT requests).req:request_id()
returns a string containing the request ID.req:request_date()
returns the date as it'll be written in the Date
response header.req:is_https()
returns true
if this request is serviced through HTTPS, false
otherwise.req:host()
returns the value of the Host
header if present, otherwise nil
.req:http_version()
returns HTTP/1.0
or HTTP/1.1
depending on the request version.req:http_method()
returns a string, in uppercase, with the HTTP method (e.g. "GET"
).req:http_headers()
returns a table with all headers and their values.Handler functions may return either nil
(in which case, a 200 OK
response is generated), or a number matching an HTTP status code. Attempting to return an invalid HTTP status code or anything other than a number or nil
will result in a 500 Internal Server Error
response being thrown.
In addition to the metamethods in the req
parameter, one can also log messages with different logging levels by calling methods from Lwan.log
:
Lwan.log:warning(str)
Lwan.log:info(str)
Lwan.log:error(str)
Lwan.log:critical(str)
(Will also abort Lwan! Use with caution)Lwan.log:debug(str)
(Only available in debug builds; no-op otherwise)Note
If Lwan is built with syslog support, these messages will also be sent to the system log, otherwise they'll be printed to the standard error.
The rewrite
module will match patterns in URLs and give the option to either redirect to another URL, or rewrite the request in a way that Lwan will handle the request as if it were made in that way originally.
The new URL can be specified using a simple text substitution syntax, or use Lua scripts.
Tip
Lua scripts will contain the same metamethods available in the req
metatable provided by the Lua module, so it can be quite powerful.
Each instance of the rewrite module will require a pattern
and the action to execute when such pattern is matched. Patterns are evaluated in the order they appear in the configuration file, and are specified using nested sections in the configuration file. For instance, consider the following example, where two patterns are specified:
rewrite /some/base/endpoint {
pattern posts/(%d+) {
# Matches /some/base/endpointposts/2600 and /some/base/endpoint/posts/2600
rewrite_as = /cms/view-post?id=%1
}
pattern imgur/(%a+)/(%g+) {
# Matches /some/base/endpointimgur/gif/mpT94Ld and /some/base/endpoint/imgur/gif/mpT94Ld
redirect_to = https://i.imgur.com/%2.%1
}
}
This example defines two patterns, one providing a nicer URL that's hidden from the user, and another providing a different way to obtain a direct link to an image hosted on a popular image hosting service (i.e. requesting /some/base/endpoint/imgur/mp4/4kOZNYX
will redirect directly to a resource in the Imgur service).
The value of rewrite_as
or redirect_to
can be Lua scripts as well; in which case, the option expand_with_lua
must be set to true
, and, instead of using the simple text substitution syntax as the example above, a function named handle_rewrite(req, captures)
has to be defined instead. The req
parameter is documented in the Lua module section; the captures
parameter is a table containing all the captures, in order (i.e. captures[2]
is equivalent to %2
in the simple text substitition syntax). This function returns the new URL to redirect to.
This module has no options by itself. Options are specified in each and every pattern.
Option Type Default Descriptionrewrite_as
str
NULL
Rewrite the URL following this pattern redirect_to
str
NULL
Redirect to a new URL following this pattern expand_with_lua
bool
false
Use Lua scripts to redirect to or rewrite a request
redirect_to
and rewrite_as
options are mutually exclusive, and one of them must be specified at least.
It's also possible to specify conditions to trigger a rewrite. To specify one, open a condition
block, specify the condition type, and then the parameters for that condition to be evaluated. Multiple conditions can be set per rewrite rule as long as there's one condition per type:
cookie
Yes Yes A single key
= value
Checks if request has cookie key
has value value
query
Yes Yes A single key
= value
Checks if request has query variable key
has value value
post
Yes Yes A single key
= value
Checks if request has post data key
has value value
header
Yes Yes A single key
= value
Checks if request header key
has value value
environment
Yes Yes A single key
= value
Checks if environment variable key
has value value
stat
Yes Yes path
, is_dir
, is_file
Checks if path
exists in the filesystem, and optionally checks if is_dir
or is_file
encoding
No Yes deflate
, gzip
, brotli
, zstd
, none
Checks if client accepts responses in a determined encoding (e.g. deflate = yes
for Deflate encoding) proxied
No No Boolean Checks if request has been proxied through PROXY protocol http_1.0
No No Boolean Checks if request is made with a HTTP/1.0 client is_https
No No Boolean Checks if request is made through HTTPS has_query_string
No No Boolean Checks if request has a query string (even if empty) method
No No Method name Checks if HTTP method is the one specified lua
No No String Runs Lua function matches(req)
inside String and checks if it returns true
or false
backref
No Yes A single backref index
= value
Checks if the backref number matches the provided value
Can use subst. syntax refers to the ability to reference the matched pattern using the same substitution syntax used for the rewrite as
or redirect to
actions. For instance, condition cookie { some-cookie-name = foo-%1-bar }
will substitute %1
with the first match from the pattern this condition is related to.
Note
Conditions that do not require a section have to be written as a key; for instance, condition has_query_string = yes
.
For example, if one wants to send site-dark-mode.css
if there is a style
cookie with the value dark
, and send site-light-mode.css
otherwise, one can write:
pattern site.css {
rewrite as = /site-dark-mode.css
condition cookie { style = dark }
}
pattern site.css {
rewrite as = /site-light-mode.css
}
Another example: if one wants to send pre-compressed files if they do exist in the filesystem and the user requested them:
pattern (%g+) {
condition encoding { brotli = yes }
condition stat { path = %1.brotli }
rewrite as = %1.brotli
}
pattern (%g+) {
condition encoding { gzip = yes }
condition stat { path = %1.gzip }
rewrite as = %1.gzip
}
pattern (%g+) {
condition encoding { zstd = yes }
condition stat { path = %1.zstd }
rewrite as = %1.zstd
}
pattern (%g+) {
condition encoding { deflate = yes }
condition stat { path = %1.deflate }
rewrite as = %1.deflate
}
Note
In general, this is not necessary, as the file serving module will do this automatically and pick the smallest file available for the requested encoding, but this shows it's possible to have a similar feature by configuration alone.
The redirect
module will, as it says in the tin, generate a 301 Moved permanently
(by default; the code can be changed, see below) response, according to the options specified in its configuration. Generally, the rewrite
module should be used instead as it packs more features; however, this module serves also as an example of how to write Lwan modules (less than 100 lines of code).
If the to
option is not specified, it always generates a 500 Internal Server Error
response. Specifying an invalid HTTP code, or a code that Lwan doesn't know about (see enum lwan_http_status
), will produce a 301 Moved Permanently
response.
to
str
NULL
The location to redirect to code
int
301
The HTTP code to perform a redirect
The response
module will generate an artificial response of any HTTP code. In addition to also serving as an example of how to write a Lwan module, it can be used to carve out voids from other modules (e.g. generating a 405 Not Allowed
response for files in /.git
, if /
is served with the serve_files
module).
If the supplied code
falls outside the response codes known by Lwan, a 404 Not Found
error will be sent instead.
code
int
999
A HTTP response code
The fastcgi
module proxies requests between the HTTP client connecting to Lwan and a FastCGI server accessible by Lwan. This is useful, for instance, to serve pages from a scripting language such as PHP.
Note
This is a preliminary version of this module, and as such, it's not well optimized, some features are missing, and some values provided to the environment are hardcoded.
Option Type Default Descriptionaddress
str
Address to connect to. Can be a file path (for Unix Domain Sockets), IPv4 address (aaa.bbb.ccc.ddd:port
), or IPv6 address ([...]:port
). script_path
str
Location where the CGI scripts are located. default_index
str
index.php
Default script to execute if unspecified in the request URI.
Authorization sections can be declared in any module instance or handler, and provides a way to authorize the fulfillment of that request through the standard HTTP authorization mechanism. In order to require authorization to access a certain module instance or handler, declare an authorization
section with a basic
parameter, and set one of its options.
realm
str
Lwan
Realm for authorization. This is usually shown in the user/password UI in browsers password_file
str
NULL
Path for a file containing username and passwords (in clear text). The file format is the same as the configuration file format used by Lwan
Warning
Not only passwords are stored in clear text in a file that should be accessible by the server, they'll be kept in memory for a few seconds. Avoid using this feature if possible.
Please read this section (and follow it) if you're planning on contributing to Lwan. There's nothing unexpected here; this mostly follows the rules and expectations of many other FOSS projects, but every one expects things a little bit different from one another.
Lwan tries to follow a consistent coding style throughout the project. If you're considering contributing a patch to the project, please respect this style by trying to match the style of the surrounding code. In general:
global_variables_are_named_like_this
, even though they tend to be rare and should be marked as static
(with rare exceptions)local_var
, i
, conn
typedef
for structs are rarely used in Lwan#pragma once
instead of the usual include guard hackerylwan-private.h
lwan_
lwan_
lwan_
prefix/* Old C-style comments are preferred */
clang-format
can be used to format the source code in an acceptable way; a .clang-format
file is providedIf modifying well-tested areas of the code (e.g. the event loop, HTTP parser, etc.), please add a new integration test and make sure that, before you send a pull request, all tests (including the new ones you've sent) are working. Tests can be added by modifying src/scripts/testsuite.py
, and executed by either invoking that script directly from the source root, or executing the testsuite
build target.
Some tests will only work on Linux, and won't be executed on other platforms.
Lwan is automatically fuzz-tested by OSS-Fuzz. To fuzz-test locally, though, one can follow the instructions to test locally.
Currently, there are fuzzing drivers for the request parsing code, the configuration file parser, the template parser, and the Lua string pattern matching library used in the rewrite module.
Adding new fuzzers is trivial:
src/bin/fuzz
.${FUZZER_NAME}_fuzzer.cc
. Look at the OSS-Fuzz documentation and other fuzzers on information about how to write these.src/fuzz/corpus
. Files have to be named corpus-${FUZZER_NAME}-${UNIQUE_ID}
.The shared object version of liblwan
on ELF targets (e.g. Linux) will use a symbol filter script to hide symbols that are considered private to the library. Please edit src/lib/liblwan.sym
to add new symbols that should be exported to liblwan.so
.
Lwan tries to maintain a source history that's as flat as possible, devoid of merge commits. This means that pull requests should be rebased on top of the current master before they can be merged; sometimes this can be done automatically by the GitHub interface, sometimes they need some manual work to fix conflicts. It is appreciated if the contributor fixes these conflicts when asked.
It is advisable to push your changes to your fork on a branch-per-pull request, rather than pushing to the master
branch; the reason is explained below.
Please ensure that Git is configured properly with your name (it doesn't really matter if it is your legal name or a nickname, but it should be enough to credit you) and a valid email address. There's no need to add Signed-off-by
lines, even though it's fine to send commits with them.
If a change is requested in a pull request, you have two choices:
It is not enforced, but it is recommended to create smaller commits. How commits are split in Lwan is pretty much arbitrary, so please take a look at the commit history to get an idea on how the division should be made. Git offers a plethora of commands to achieve this result: the already mentioned interactive rebase, the -p
option to git add
, and git commit --amend
are good examples.
Commit messages should have one line of summary (~72 chars), followed by an empty line, followed by paragraphs of 80-char lines explaining the change. The paragraphs explaining the changes are usually not necessary if the summary is good enough. Try to write good commit messages.
Lwan is licensed under the GNU General Public License, version 2, or (at your option), any later version. Therefore:
While Lwan was written originally for Linux, it has been ported to BSD systems as well. The build system will detect the supported features and build support library functions as appropriate.
For instance, epoll has been implemented on top of kqueue, and Linux-only syscalls and GNU extensions have been implemented for the supported systems. This blog post explains the details and how #include_next
is used.
It can achieve good performance, yielding about 320000 requests/second on a Core i7 laptop for requests without disk access, and without pipelining.
When disk I/O is required, for files up to 16KiB, it yields about 290000 requests/second; for larger files, this drops to 185000 requests/second, which isn't too shabby either.
These results, of course, with keep-alive connections, and with weighttp running on the same machine (and thus using resources that could be used for the webserver itself).
Without keep-alive, these numbers drop around 6-fold.
There is an IRC channel (#lwan
) on Libera. A standard IRC client can be used.
Here's a non-definitive list of third-party stuff that uses Lwan and have been seen in the wild. If you see mentions of Lwan in the media or academia, however small it might be, please contact the author! It'll make her day!
Some other distribution channels were made available as well:
Dockerfile
is maintained by @jaxgeller, and is available from the Docker registry.Lwan has been also used as a benchmark:
Mentions in academic journals:
Mentions in magazines:
Mentions in books:
Some talks mentioning Lwan:
Not really third-party, but alas:
Lwan container images are available at ghcr.io/lpereira/lwan. Container runtimes like Docker or Podman may be used to build and run Lwan in a container.
Pull lwan images from GHCRContainer images are tagged with release version numbers, so a specific version of Lwan can be pulled.
# latest version
docker pull ghcr.io/lpereira/lwan:latest
# pull a specific version
docker pull ghcr.io/lpereira/lwan:v0.3
Clone the repository and use Containerfile
(Dockerfile) to build Lwan with all optional dependencies enabled.
The image expects to find static content at /wwwroot
, so a volume containing your content can be mounted.
docker run --rm -p 8080:8080 -v ./www:/wwwroot lwan
To bring your own lwan.conf
, simply mount it at /lwan.conf
.
podman run --rm -p 8080:8080 -v ./lwan.conf:/lwan.conf lwan
Run image with socket activation on a Linux host with Podman
Podman supports socket activation of containers. This example shows how to run lwan with socket activation and Podman on a Linux host.
Requirements: Podman version 4.5.0 or higher.
sudo machinectl shell test@
podman build -t lwan ~/lwan
mkdir -p ~/.config/containers/systemd
mkdir -p ~/.config/systemd/user
listener systemd:my.socket
site {
serve_files / {
path = /web
}
}
[Socket]
ListenStream=8080
[Unit]
After=my.socket
Requires=my.socket
[Container]
Network=none
Image=localhost/lwan
Volume=/home/test/lwan.conf:/lwan.conf:Z
Volume=/home/test/web:/web:Z
The option :Z
is needed on SELinux systems. As lwan only needs to communicate over the socket-activated socket, it's possible to use Network=none
. See the article How to limit container privilege with socket activation.mkdir ~/web
echo hello > ~/web/file.txt
systemctl --user daemon-reload
systemctl --user start my.socket
$ curl localhost:8080/file.txt
hello
These are some of the quotes found in the wild about Lwan. They're presented in no particular order. Contributions are appreciated:
"Lwan is like a classic, according to the definition given by Italian -- writer Italo Calvino: you can read it again and again" Antonio Piccolboni
"I read lwan's source code. Especially, the part of using coroutine was very impressive and it was more interesting than a good novel. Thank you for that." -- @patagonia
"For the server side, we're using Lwan, which can handle 100k+ reqs/s. It's supposed to be super robust and it's working well for us." -- @fawadkhaliq
"Insane C thing" -- Michael Sproul
"The best performer is LWAN, a newcomer" -- InfoQ
"I've never had a chance to thank you for Lwan. It inspired me a lot to develop Zewo" -- @paulofariarl
"Let me say that lwan is a thing of beauty. I got sucked into reading the source code for pure entertainment, it's so good. high five" -- @kwilczynski
"mad science" -- jwz
"Nice work with Lwan! I haven't looked that carefully yet but so far I like what I saw. You definitely have the right ideas." -- @thinkingfish
"Lwan is a work of art. Every time I read through it, I am almost always awe-struck." -- @neurodrone
"For Round 10, Lwan has taken the crown" -- TechEmpower
"Jeez this is amazing. Just end to end, rock solid engineering. (...) But that sells this work short." kjeetgill
"I am only a spare time C coder myself and was surprised that I can follow the code. Nice!" cntlzw
"Impressive all and all, even more for being written in (grokkable!) C. Nice work." tpaschalis
"LWAN was a complete failure" dermetfan
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4