# Luan websites A Luan website is a directory, usually `src/`, served by Luan's web server. This file covers how requests are served, static sites, and hosting. Sites that store data are in [dynamic-sites.md](dynamic-sites.md). The `Http` module is described in [library/http/Http.md](library/http/Http.md). ## How a URL finds its code - A URL maps to `site:` plus the path plus `.luan`: `/x/page.html` runs `src/x/page.html.luan`, and `/x/run` runs `src/x/run.luan`. - A path ending in `/` tries `index.html` first, meaning a static `src/index.html` or `src/index.html.luan`. If neither exists, the directory is listed. So `/` is usually `src/index.html.luan`. - A file without `.luan` is served as it is: `/site.css` is `src/site.css`. A path that is neither runs `Http.not_found_handler` if the site defines one, otherwise it's a 404. - A page module returns a function, which the server calls for each request. It reads `Http.request` and writes to `Http.response`. If the function writes nothing, the response is empty. - `init.luan` in `src/`, if present, runs once when the site starts. It is optional; blog has none. ## How a request reaches a site The server is goodjava's webserver, where a handler takes a `Request` struct and returns a `Response` struct, or `null` if it doesn't handle the request. Luan chains these handlers (`src/luan/modules/http/Server.luan` locally, `src/luan/host/WebHandler.java` when hosted): 1. `LogHandler` logs the request. 2. `SafeHandler` rejects any path containing `..` with 400, turns an exception into a 500 page, and turns "no handler" into a 404. 3. `HeadersHandler`, then, when hosted, the password check for `/private/`. 4. The Luan page or the static file (for a path ending in `/`, its `index.html`), then a directory listing, then `Http.not_found_handler`. `ContentTypeHandler` wraps the page, file and not-found handlers, and sets `Content-Type` from the path's extension when the response has none (`.luan` is `text/plain`, `.md` is `text/markdown`, `.html` is HTML). So a handler that serves a file needn't set the type. Check this chain before deciding that a response lacks a header. ## The request model Each page module is loaded once into the site's main interpreter, and cached. Each request then runs the page's function in a fresh clone of that interpreter, so per-request state is just per-interpreter state and needs no synchronization. Because of the cache, restart the server after changing code. ## A page ```lua local Luan = require "luan:Luan.luan" local error = Luan.error local Io = require "luan:Io.luan" local Http = require "luan:http/Http.luan" local Shared = require "site:/lib/Shared.luan" local head = Shared.head or error() local header = Shared.header or error() return function() Io.stdout = Http.response.text_writer() %> <!doctype html> <html lang="en"> <head> <% head() %> <title>About</title> </head> <body> <% header() %> <div content> <h1>About</h1> <p>...</p> </div> </body> </html> <% end ``` `lib/Shared.luan` holds what the site's pages share. `Shared.head()` writes the `<meta>` and stylesheet lines, and `Shared.header()` the top of each page: ```lua local Shared = {} local started = Time.now() function Shared.head() %> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> @import "/site.css?s=<%=started%>"; </style> <script src="/site.js?s=<%=started%>"></script> <% end return Shared ``` The `?s=<%=started%>` changes each time the server starts, so browsers fetch new copies of the CSS and JavaScript after a change. The `@import` is used instead of `<link rel="stylesheet">` because it is easier to read; the small cost in speed doesn't matter. `lib/Utils.luan` holds general helpers that know nothing about the site, so it could be copied to any site. `Shared.luan` holds what is particular to this site, and may require `Utils`, never the other way round. ## Static sites The luan website (`website/src` in the luan repository) and the Reactionary Software site are static sites written in Luan: pages are generated, but nothing is stored. - Most pages are a returned function with one template, as above. - Documentation pages build a table of sections, each with a `title`, a `content` function holding a template, and optional `subs`, and render it with `Shared.show_toc` and `Shared.show_content` (see `website/src/docs.html.luan` and `manual.html.luan`). - Small working examples are in `website/src/examples`: forms (`hi2`), chunked output, server-sent events (`sse_push`, `sse_pull`), file upload and mail (`upload-and-email`). - These sites are in English only; readers use their browser's translation. Text that a translator must not change is marked `translate="no"`: every `<code>` and `<pre>`. Code-like text inside prose is `<code bold translate="no">` or `<code italic translate="no">`, styled like ordinary bold and italic. Plain `<b translate="no">` isn't enough, because translators then mangle the spacing around it. Browser translation is still poor, so this is not finished. ## Hosting Luan sites are hosted on Luan's own hosting service. - `luan luan:host/push.luan <domain> <password> src` publishes a directory to a hosted site: it copies new and changed files, creates directories, deletes remote files that no longer exist locally, and reloads the site. Projects have scripts for this, like `push-test.sh`. Directories named `local` are never pushed or deleted, so a site keeps its runtime data under `private/local/` and the data survives pushes. - When hosted, `src/luan/host/init.luan` runs before the site. It calls `Http.set_domain(domain)`, sets `Hosted.is_hosted = true`, and defines functions like `Hosted.set_https`. Site code calls such functions only if they exist: `Hosted.set_https and Hosted.set_https(...)`. - `/private/` is protected with HTTP authentication when hosted (user `admin`, the site's password). `Io.password` is that password when hosted, and `"password"` locally. - A site can add its own functions to `Rpc.functions`, callable from outside. The host doesn't check the password for these, so each must check it itself: `Io.password == password or error "wrong password"`. - Hosted sites can't use Java directly, and `file:` URIs must stay inside the site's directory. The standard library uses Java on the site's behalf. - The domain `www.me.luan.software` maps to localhost, for testing hosting locally. ## Security - Escape everything shown in HTML with `Html.encode`, including values from the request. `Html.encode` escapes `<`, `>`, `"` and entity-like `&`, but not `'`, so use it only in text and in double-quoted attributes. It errors on `nil`. - Better still, write values from the database rather than from the request: `post.id`, not the `post` parameter. A successful lookup doesn't make a parameter safe, because Lucene's query parser accepts extra terms: `?post=3 \"><script>...` still finds post 3. - A URL supplied by a user needs its scheme checked (`starts_with(url,"http")`), because encoding doesn't stop `javascript:` in an `href`. - Code that maps a request path to a file is protected from `..` by `SafeHandler`. Test any such code with `curl --path-as-is` using both `..` and `%2e%2e`; browsers remove `..` before sending, so they never show the problem. - For a check that a path stays inside a directory, compare canonical paths (Java `getCanonicalPath()`), and match the directory plus `/`, so `site_other` doesn't match `site`.