Skip to content

sad-pixel/wtfhttpd

Repository files navigation

WtfHttpd

The HTTP server that makes you go wtf!

Logo


This is obviously not production ready, nor is it complete, though the features described here do work.

wtfhttpd is a cgi-esque web server written in Go that asks: what if your SQL script was your backend?

It uses file-based routing to map HTTP requests directly to .sql files, executes them against a SQLite database, and renders the results as either JSON or HTML via templates.

In other words, it treats HTTP communication as a database transaction: you read the state of the world from a set of input tables and write your intended changes to an output table.

File Based Routing

Routes are determined by the filesystem layout within the webroot directory.

  • webroot/index.sql -> ANY /
  • webroot/users.sql -> ANY /users
  • webroot/users.get.sql -> GET /users
  • webroot/users.post.sql -> POST /users

Supported methods are .get, .post, .put, .patch, .delete, .options. Files without a method extension respond to any HTTP method.

wtfhttpd supports path parameters for dynamic routes. If a request is made to /users/123, it will be handled by webroot/users/{id}.get.sql and the parameter will be available in the path_params table.

Request Context

For each request, wtfhttpd creates a set of temporary tables that you can query.

  • path_params: Contains URL path parameters.
    • SELECT value FROM path_params WHERE name = 'id'
  • query_params: Contains URL query string parameters.
    • SELECT value FROM query_params WHERE name = 'search'
  • request_headers: Contains all HTTP request headers.
  • request_form: Contains all request form data fields (uploads aren't supported yet!)
  • request_meta: Contains metadata like method, path, and remote_addr.
  • env_vars: Contains environment variables from the server process that match the env_prefix from config.
  • request_cookies: Contains all cookies sent in the request headers.
  • request_json: Contains the flattened key-value representation of a JSON request body. This table is only populated for requests with a Content-Type: application/json header.

The request_json table has the following schema:

Column Type Description
path TEXT A JSONPath-like string representing the unique key for a value. e.g., $.user.name or $.tags[0].
value ANY The primitive value of the key. For objects and arrays, this will be a string like [object] or [array].
type TEXT The original JSON type: object, array, string, number, boolean, or null.
json TEXT If the value is an object or array, this column contains the raw JSON string of that sub-tree, allowing you to use SQLite's built-in json_extract functions on nested data. For other types, this is NULL.

JSON Request Body Parsing

wtfhttpd automatically parses JSON request bodies for requests that have a Content-Type header of application/json.

The parsed JSON is flattened and inserted into a temporary table called request_json.

For example, given this incoming JSON body:

{
  "name": "Alice",
  "email": "alice@example.com",
  "tags": ["user", "beta"],
  "settings": {
    "theme": "dark"
  }
}

You could extract the user's name with the following query:

SELECT value FROM request_json WHERE path = '$.name';
-- Result: 'Alice'

Parameter Binding

Path parameters, form data fields, and query parameters are also provided as named parameters to the SQL files. For example, a query param called name can be referred to as @name in the SQL file, instead of having to write SELECT value FROM query_params WHERE name = 'name'

There is special handling for form data and query params that end with [], which are intended for array-like use cases (example: checkboxes). wtfhttpd will strip the [] and encode a json array, which can be accessed in SQL as json_each(@id), assuming the param was called id[]

The order of precedence is Path > Form > Query.

Early Returning

Routes can terminate early by calling the special function wtf_abort. An optional http code and message can be passed to it.

Response Handling

By default, the result of your final query is returned as application/json.

To control the response, you can INSERT into the response_meta table:

  • Set HTTP Status: INSERT INTO response_meta VALUES ('status', '404');
  • Set a Header: INSERT INTO response_meta VALUES ('Content-Type', 'text/plain');
  • Render a Template: INSERT INTO response_meta VALUES ('wtf-tpl', 'path/to/template.html');

Cookie Handling

Reading Incoming Cookies

For every request, wtfhttpd parses the Cookie header and populates a temporary, read-only table called request_cookies.

Column Type Description
name TEXT The name of the cookie.
value TEXT The delicious, crumbly value.

So, to get a session token, you can simply:

SELECT value FROM request_cookies WHERE name = 'session_token';

Setting Outgoing Cookies

To set cookies, you can INSERT into the response_cookies table. wtfhttpd will intercept these inserts and transform them into proper Set-Cookie headers.

The response_cookies Schema:

Column Type Description Default
name TEXT The cookie's name. (Required)
value TEXT The cookie's value. (Required)
max_age INTEGER Lifetime in seconds. A negative value deletes the cookie. NULL
expires TIMESTAMP Absolute expiration time. NULL
path TEXT The scope of the cookie. '/'
domain TEXT The domain scope. NULL
secure INTEGER 1 for HTTPS only. Default is 0 for localhost dev. 0
http_only INTEGER 1 makes it inaccessible to JavaScript. A good idea. 1
same_site TEXT CSRF protection. 'Strict', 'Lax', or 'None'. 'Lax'

Here's an example of setting cookies:

-- Set the cookie for the browser.
INSERT INTO response_cookies (name, value, max_age, secure, http_only, same_site)
VALUES ('session_token', @session_token, 86400, 1, 1, 'Strict'); -- 24 hours, HTTPS only, extra secure

SQL Directives

Directives apply at a query level, and are parsed from SQL comments.

The available directives are:

  • @wtf-validate <param_name> <validation_rule>: Used for performing input validation. param_name corresponds to the name of a bound variable, validation_rule is a string that specifies one or more rules that are comma separated.
    • Example: -- @wtf-validate name required,min=5 will validate that
      • A named parameter @name is present
      • It has a minimum length of 5
      • In case the validation fails, a HTTP 400 (Bad Request) will be returned.
      • Validation rules follow this syntax
  • @wtf-store <variable_name>: Puts the results of that query into the variable name requested, instead of into ctx. This is useful for binding multiple queries to separate things that can be referred to in the templates or JSON responses.
  • @wtf-capture <variable name> [single]: Puts the result of the query into a named parameter with the variable name requested. This is useful for referring to the value in later queries. If "single" is provided as the second argument, the result named parameter is bound as a scalar. If the second argument is not single or not provided, the named parameter is bound as a json encoded string of the query results.

Templating

Templates use jinja2 syntax (via Gonja), and can be anywhere in the webroot, but must have a ".tpl" somewhere in the filename.

A default variable called ctx is present in the template's context, which will contain the results of the last query. Any stored variables created from @wtf-store are also available.

Additional Functions

The following extra functions are available inside the sql environment:

  • slugify(path) - Returns a slug version of the given path
  • bcrypt_hash(password, [cost]) - Creates a hash for secrets
  • bcrypt_verify(password, hash) - Verifies bcrypt hashed secrets
  • checksum_md5(content) - Creates a md5 checksum (DO NOT USE FOR PASSWORDS)
  • checksum_sha1(content) - Creates a sha1 checksum (DO NOT USE FOR PASSWORDS)
  • cache_get(key) - Fetch a value from the in-memory cache
  • cache_set(key, value) - Store a value in the in-memory cache
  • cache_delete(key) - Delete a key from the in-memory cache
  • secure_hex(len) - Creates a cryptographically secure hex string of the specified length
  • build_query(json_object) - Converts a JSON object to a URL query string
  • parse_query(query_string) - Converts a URL query string to a JSON object
  • http_get(url, [headers_json]) - Makes a GET request to the specified URL
  • http_post(url, [headers_json], [body]) - Makes a POST request to the specified URL
  • http_put(url, [headers_json], [body]) - Makes a PUT request to the specified URL
  • http_patch(url, [headers_json], [body]) - Makes a PATCH request to the specified URL
  • http_delete(url, [headers_json]) - Makes a DELETE request to the specified URL
  • time_now([format]) - Returns the current time in the specified format (default: "2006-01-02 15:04:05")
  • time_format(time_str, target_format, [source_format]) - Formats a time string from one format to another (default source format: "2006-01-02 15:04:05")
  • time_add(time_str, duration_str, [format]) - Adds a duration to a time string (e.g., "1h30m", "-24h")
  • time_diff(time_str1, time_str2, [format]) - Returns the difference between two time strings as a duration
  • time_relative(time_str, [format]) - Returns a human-readable relative time string (e.g., "5 minutes ago", "in 2 days")

HTTP Client

The following functions are available for use in SQL:

  • http_get(url, [headers_json])
  • http_post(url, [headers_json], [body])
  • http_put(url, [headers_json], [body])
  • http_patch(url, [headers_json], [body])
  • http_delete(url, [headers_json])

Function Arguments:

  • url (TEXT): The full URL to request.
  • headers_json (TEXT, Optional): A JSON string representing the headers to send. Example: '{"Authorization": "Bearer ...", "Accept": "application/json"}'.
  • body (TEXT, Optional): The request body for POST, PUT, and PATCH.

All these functions return a single TEXT value: a JSON string containing the entire response. You can parse the details you need using SQLite's built-in json_extract function.

The returned JSON has the following schema:

{
  "status_code": 200,
  "status": "200 OK",
  "headers": {
    "Content-Type": ["application/json; charset=utf-8"],
    "Content-Length": ["1234"],
    "X-RateLimit-Remaining": ["4999"]
  },
  "body": "{\"login\":\"octocat\",\"id\":1,\"name\":\"The Octocat\"...}",
  "error": null
}

If a network error occurs (e.g., DNS failure, timeout), the body would be null and the error field would contain the error message.

Security Notes

wtfhttpd takes security seriously. SQL injection is impossible by design, as everything is either table values or named parameters, and it is impossible for a developer to perform any sort of string concatenation to create sql queries. Values passed to templates are also escaped by default, and there is no way to turn this off.

Route introspection

All registered routes are available in the wtf_routes table. This is used in the admin interface, but is also available for sql scripts to query.

Admin Interface

An admin interface protected by HTTP Basic auth is available at /_wtf. The admin interface shows some basic statistics like uptime and hits, registered routes, and has a database schema viewer, data viewer, and SQL query console.

Configuration

wtfhttpd may be configured using a wtf.toml config file. The default values are shown below

host = "127.0.0.1"
port = 8080
db = "wtf.db"
web_root = "webroot"
live_reload = true
enable_admin = true
admin_username = "wtfhttpd"
admin_password = "wtfhttpd"

load_dotenv = true
env_prefix = "WTF_"

Misc Notes

  • Every request runs in it's own transaction, and since sqlite doesn't support nested transactions, you may not use transactions in your sql queries.
  • A / is always added to the end of every path. This is may be fixed later.

License

MIT

Copyright 2025 Ishan Das Sharma

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

The HTTP server that makes you go wtf!

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published