From a461d371dfec255af572a6e8d4a1988261f603ae Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Wed, 4 Mar 2026 10:16:27 +0100 Subject: [PATCH 1/3] feat(request): add a query method It was a field on the object, this adds a 'getter', similar to the method, path, etc. --- example/querystring.lua | 2 +- spec/unit/request_spec.lua | 2 +- src/pegasus/request.lua | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/example/querystring.lua b/example/querystring.lua index 84ddb59..2d8ba04 100644 --- a/example/querystring.lua +++ b/example/querystring.lua @@ -14,7 +14,7 @@ local printTable = function(table) end server:start(function (req, res) - printTable(req['querystring']) + printTable(req:query()) res:addHeader('Content-Type', 'text/html'):write('hello pegasus world!') -- return a truthy value to indicate the request was handled, no further handling needed diff --git a/spec/unit/request_spec.lua b/spec/unit/request_spec.lua index f2841e0..711d4d5 100644 --- a/spec/unit/request_spec.lua +++ b/spec/unit/request_spec.lua @@ -75,7 +75,7 @@ describe('require', function() ) assert.are.same( - request.querystring, { + request:query(), { a = 'b', c = 'd', e = { '1', '2' }, diff --git a/src/pegasus/request.lua b/src/pegasus/request.lua index c6579e2..111388c 100644 --- a/src/pegasus/request.lua +++ b/src/pegasus/request.lua @@ -215,7 +215,15 @@ function Request:method() return self._method end ---- Read and parse headers. +--- Query-string parameters. +-- Returns a hash table. Duplicate keys are represented as an array of values. +-- @treturn table querystring +function Request:query() + self:parseFirstLine() + return self.querystring +end + +--- Request headers. -- Returns a case-insensitive table: looking up with any casing works. -- If a header appears multiple times, the value becomes a table of strings. -- @treturn table headers From c47eb60d52cd822ac80d345d7f0621a86d16b5e4 Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Sun, 29 Mar 2026 15:19:18 +0200 Subject: [PATCH 2/3] chore(docs): create ldoc based documentation --- config.ld | 16 + doc_topics/ldoc.css | 618 ++++++++++++++++++++++++++++++ src/pegasus/handler.lua | 91 ++--- src/pegasus/init.lua | 84 ++-- src/pegasus/log.lua | 6 +- src/pegasus/plugins/compress.lua | 24 +- src/pegasus/plugins/downloads.lua | 39 +- src/pegasus/plugins/files.lua | 27 +- src/pegasus/plugins/router.lua | 147 ++++--- src/pegasus/plugins/tls.lua | 18 +- src/pegasus/request.lua | 93 ++--- src/pegasus/response.lua | 149 +++---- 12 files changed, 881 insertions(+), 431 deletions(-) create mode 100644 config.ld create mode 100644 doc_topics/ldoc.css diff --git a/config.ld b/config.ld new file mode 100644 index 0000000..d903da2 --- /dev/null +++ b/config.ld @@ -0,0 +1,16 @@ +project='Pegasus' +title='Pegasus webserver in Lua' +description='Minimal, embeddable HTTP server with a simple plugin system.' + +format='markdown' +use_markdown_titles = true +style="./doc_topics/" + +file={'./src/'} +topics={'./LICENSE', './README.md'} +examples = {'./example'} + +dir='docs' +sort=true +sort_modules=true +all=false diff --git a/doc_topics/ldoc.css b/doc_topics/ldoc.css new file mode 100644 index 0000000..df2c98f --- /dev/null +++ b/doc_topics/ldoc.css @@ -0,0 +1,618 @@ +/* Pegasus LDoc — modern documentation theme (CSS only) */ +:root { + --bg-page: #f4f6f9; + --bg-surface: #ffffff; + --bg-sidebar: #fafbfc; + --border: #e2e8f0; + --border-strong: #cbd5e1; + --text: #1e293b; + --text-muted: #64748b; + --accent: #2563eb; + --accent-hover: #1d4ed8; + --accent-soft: rgba(37, 99, 235, 0.08); + --accent-ring: rgba(37, 99, 235, 0.35); + --header-ink: #0f172a; + --radius: 10px; + --radius-sm: 6px; + --shadow: 0 1px 3px rgba(15, 23, 42, 0.06); + --shadow-md: 0 4px 24px rgba(15, 23, 42, 0.08); + --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + --font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Source Code Pro", + Menlo, Consolas, monospace; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + color: var(--text); + font-size: 16px; + line-height: 1.6; + font-family: var(--font-sans); + background: var(--bg-page); + -webkit-font-smoothing: antialiased; +} + +#product { + display: none; +} + +#container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +#main { + display: flex; + flex: 1; + align-items: flex-start; + width: 100%; +} + +a:link, +a:visited { + color: var(--accent); + text-decoration-skip-ink: auto; +} + +a:hover { + color: var(--accent-hover); + text-decoration: underline; + text-underline-offset: 2px; +} + +h1 { + font-size: 1.75rem; + font-weight: 650; + letter-spacing: -0.02em; + color: var(--header-ink); +} + +h2 { + font-size: 1.35rem; + font-weight: 600; + letter-spacing: -0.015em; + color: var(--header-ink); +} + +h3 { + font-size: 1.125rem; + font-weight: 600; + color: var(--header-ink); +} + +h4 { + font-size: 1rem; + font-weight: 600; + color: var(--header-ink); +} + +hr { + height: 1px; + background: var(--border); + border: 0; + margin: 1.25rem 0; +} + +code, +tt { + font-family: var(--font-mono); + font-size: 0.9em; + background: var(--accent-soft); + color: var(--header-ink); + padding: 0.15em 0.4em; + border-radius: 4px; +} + +#content h1 code { + background: rgba(255, 255, 255, 0.15); + color: inherit; + padding: 0.1em 0.35em; +} + +span.parameter { + font-family: var(--font-mono); + font-weight: 600; + color: var(--text-muted); +} + +span.parameter::after { + content: ":"; +} + +span.types::before { + content: "("; +} + +span.types::after { + content: ")"; +} + +.type { + font-weight: 600; + font-style: italic; + color: var(--text-muted); +} + +p.name { + font-family: var(--font-mono); + font-size: 0.95em; +} + +#navigation { + float: none; + flex: 0 0 min(17rem, 42vw); + max-width: 17rem; + vertical-align: top; + overflow: visible; + background: + linear-gradient(180deg, rgba(37, 99, 235, 0.06), rgba(37, 99, 235, 0)) + 0 0 / 100% 240px no-repeat, + var(--bg-sidebar); + border-right: 1px solid var(--border); + box-shadow: var(--shadow); + position: sticky; + top: 0; + align-self: stretch; + max-height: 100vh; + overflow-y: auto; +} + +#navigation br { + display: none; +} + +#navigation h1 { + margin: 0; + padding: 1.25rem 1.25rem 1rem; + font-size: 1.1rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--header-ink); + background: transparent; + border-bottom: 1px solid var(--border); +} + +#navigation h2 { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + background: transparent; + border-bottom: none; + padding: 1.25rem 1.25rem 0.5rem; + margin: 0; +} + +#navigation ul + h2 { + border-top: 1px dashed var(--border); + margin-top: 0.25rem; +} + +#content { + float: none; + margin-left: 0; + flex: 1; + min-width: 0; + background: var(--bg-surface); + border-radius: 0; + box-shadow: var(--shadow-md); +} + +#content h1 { + margin: 0; + padding: 1.35rem 1.5rem; + background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%); + color: #f8fafc; + font-weight: 650; + letter-spacing: -0.02em; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +#content h2 { + margin: 0; + padding: 0.85rem 1.5rem; + background: var(--bg-page); + color: var(--header-ink); + font-weight: 600; + font-size: 1.15rem; + border-bottom: 1px solid var(--border); + border-top: 1px solid var(--border); +} + +#content h2:first-of-type { + border-top: none; +} + +#content h2 a { + background: transparent; + color: inherit; + text-decoration: none; +} + +#content h2 a:hover { + color: var(--accent); + text-decoration: none; +} + +#content h3 { + font-style: normal; + margin: 1.5rem 1.5rem 0.5rem; + padding: 0 0 0.5rem; + border-bottom: 2px solid var(--border); + color: var(--header-ink); +} + +#content h4 { + margin: 1.25rem 1.5rem 0.35rem; + padding: 0 0 0.35rem; + border-bottom: 1px solid var(--border); +} + +#content pre { + margin: 1rem 1.5rem; +} + +pre code { + background: transparent; + padding: 0; + color: inherit; + font-size: inherit; + border-radius: 0; +} + +pre { + background: #1a1d26; + color: #e2e8f0; + border-radius: var(--radius); + padding: 1rem 1.15rem; + overflow: auto; + font-family: var(--font-mono); + font-size: 0.875rem; + line-height: 1.55; + border: 1px solid rgba(255, 255, 255, 0.06); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); +} + +#content ul pre.example { + margin-left: 0; +} + +table.index { + width: 100%; +} + +table.index td { + text-align: left; + vertical-align: top; +} + +#navigation ul { + font-size: 0.9375rem; + list-style: none; + margin: 0 0 0.75rem; + padding: 0.25rem 0.75rem 0.75rem; +} + +#navigation li { + text-indent: 0; + display: block; + margin: 0.125rem 0; +} + +#navigation li a { + position: relative; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.7rem; + border-radius: 10px; + color: var(--text); + text-decoration: none; + border: 1px solid transparent; + transition: background-color 0.15s ease, border-color 0.15s ease, + transform 0.05s ease; +} + +#navigation li a:hover { + text-decoration: none; + background: var(--accent-soft); + border-color: rgba(37, 99, 235, 0.18); + transform: translateY(-1px); +} + +#navigation li a:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--accent-ring); +} + +#navigation li li a { + margin: 0; +} + +/* Current page: LDoc renders it as inside the list */ +#navigation li strong { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.7rem; + border-radius: 10px; + border: 1px solid transparent; + background: rgba(37, 99, 235, 0.08); + color: var(--accent-hover); + font-weight: 700; + box-shadow: inset 3px 0 0 rgba(37, 99, 235, 0.55); +} + +#content p { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +#content table { + padding-left: 1.5rem; + padding-right: 1.5rem; + background: transparent; +} + +#content p, +#content table, +#content ol, +#content ul, +#content dl { + max-width: 52rem; +} + +#about { + padding: 1rem 1.5rem; + padding-left: 1.5rem; + background: var(--bg-surface); + border-top: 1px solid var(--border); + color: var(--text-muted); + font-size: 0.875rem; +} + +#about a { + color: var(--accent); +} + +table.module_list, +table.function_list { + border-collapse: separate; + border-spacing: 0; + margin: 1rem 1.5rem; + width: calc(100% - 3rem); + max-width: 52rem; + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + box-shadow: var(--shadow); +} + +table.module_list tr:nth-child(even) td, +table.function_list tr:nth-child(even) td { + background: #f8fafc; +} + +table.module_list td, +table.function_list td { + padding: 0.65rem 1rem; + border: none; + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +table.module_list tr:last-child td, +table.function_list tr:last-child td { + border-bottom: none; +} + +table.module_list td.name, +table.function_list td.name { + background: transparent; + min-width: 200px; + border-right: 1px solid var(--border); + font-family: var(--font-mono); + font-size: 0.9em; +} + +table.module_list td.summary, +table.function_list td.summary { + background: transparent; + width: 100%; + color: var(--text-muted); +} + +dl.function { + margin: 1rem 1.5rem 1.5rem; + border: none; + border-radius: 0; + background: transparent; + box-shadow: none; + overflow: visible; +} + +dl.function dt { + color: var(--accent); + font-family: var(--font-mono); + font-size: 0.9em; + border: 1px solid var(--border); + border-bottom: none; + border-radius: var(--radius) var(--radius) 0 0; + padding: 0.9rem 1.15rem; + background: linear-gradient(180deg, #f8fafc 0%, #fff 100%); + box-shadow: var(--shadow); + margin-top: 1rem; +} + +dl.function dt:first-child { + margin-top: 0; +} + +dl.function dd { + margin: 0; + padding: 0.75rem 1.15rem 1.25rem; + border: 1px solid var(--border); + border-top: none; + border-radius: 0 0 var(--radius) var(--radius); + background: var(--bg-surface); + box-shadow: var(--shadow); + margin-bottom: 1rem; +} + +#content dl.function dd h3 { + margin: 1.05rem 0 0.55rem; + padding: 0.85rem 0 0; + font-size: 0.9rem; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--text-muted); + border-top: 1px solid var(--border); +} + +#content dl.function dd ul, +#content dl.function dd ol { + margin: 0.25rem 0 0.75rem; + padding: 0; + padding-left: 1.25rem; + list-style-type: none; +} + +ul.nowrap { + overflow: auto; + white-space: nowrap; +} + +.section-description { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +ul ul, +ol ul, +ol ol, +ul ol { + margin-top: 0; +} + +a:target + * { + background-color: #fef9c3; + box-shadow: inset 3px 0 0 0 #eab308; + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + padding-left: 0.35rem; + margin-left: -0.35rem; + transition: background-color 0.2s ease; +} + +/* Syntax highlighting (prettify) */ +pre .comment { + color: #94a3b8; + font-style: italic; +} + +pre .constant { + color: #fbbf24; +} + +pre .escape { + color: #f472b6; +} + +pre .keyword { + color: #c084fc; + font-weight: 600; +} + +pre .library { + color: #5eead4; +} + +pre .marker { + color: #1e293b; + background: #fde047; + font-weight: 600; + padding: 0 0.15em; + border-radius: 3px; +} + +pre .string { + color: #86efac; +} + +pre .number { + color: #fdba74; +} + +pre .operator { + color: #93c5fd; + font-weight: 600; +} + +pre .preprocessor, +pre .prepro { + color: #fb923c; +} + +pre .global { + color: #e879f9; +} + +pre .user-keyword { + color: #d8b4fe; +} + +pre .prompt { + color: #4ade80; +} + +pre .url { + color: #7dd3fc; + text-decoration: underline; +} + +pre .function-name { + color: #7dd3fc; +} + +@media (max-width: 768px) { + #main { + flex-direction: column; + } + + #navigation { + position: relative; + max-height: none; + flex: none; + width: 100%; + max-width: none; + border-right: none; + border-bottom: 1px solid var(--border); + } + + #content p, + #content table, + #content ol, + #content ul, + #content dl { + max-width: none; + } + + table.module_list, + table.function_list { + width: calc(100% - 2rem); + margin-left: 1rem; + margin-right: 1rem; + } +} diff --git a/src/pegasus/handler.lua b/src/pegasus/handler.lua index 961bb29..a00ba2d 100644 --- a/src/pegasus/handler.lua +++ b/src/pegasus/handler.lua @@ -1,9 +1,10 @@ ---- Module `pegasus.handler` +--- Internal handler that drives the request/response cycle. -- -- Internal orchestrator that wires the server socket to request/response -- objects and drives the plugin pipeline. -- -- Lifecycle for each connection/request: +-- -- 1. `pluginsNewConnection(client)` can wrap/replace or reject the client -- 2. Request/Response objects are created -- 3. `pluginsNewRequestResponse(request, response)` runs @@ -13,40 +14,32 @@ -- 7. If response not closed, a default 404 is written -- -- Plugins may also: +-- -- - modify Request/Response metatables via `alterRequestResponseMetaTable` -- - intercept file processing via `processFile` -- - filter/transform streamed body via `processBodyData` -- -- Minimal plugin example: --- ```lua --- local MyPlugin = {} --- function MyPlugin:new() --- return setmetatable({}, { __index = self }) --- end --- function MyPlugin:beforeProcess(req, res) --- res:addHeader('X-Powered-By', 'Pegasus') --- end --- return MyPlugin --- ``` -- --- @module pegasus.handler +-- local MyPlugin = {} +-- +-- function MyPlugin:new() +-- return setmetatable({}, { __index = self }) +-- end +-- +-- function MyPlugin:beforeProcess(req, res) +-- res:addHeader('X-Powered-By', 'Pegasus') +-- end +-- +-- return MyPlugin +-- +-- @classmod pegasus.handler local Request = require 'pegasus.request' local Response = require 'pegasus.response' local Files = require 'pegasus.plugins.files' --- The request/response handler and plugin runner. --- --- Fields: --- - `log`: logger used by the server and plugins --- - `callback`: user callback `function(request, response)` --- - `plugins`: array of plugin instances --- --- @type Handler ----@class Handler ----@field log table ----@field callback fun(request: table, response: table)|nil ----@field plugins table local Handler = {} Handler.__index = Handler @@ -55,16 +48,11 @@ Handler.__index = Handler -- When `location` is a non-empty string, automatically enables the `files` -- plugin to serve static files from that directory (default index `/index.html`). -- --- @tparam function callback user function(request, response) +-- @tparam function callback using signature `stop_further_processing = function(request, response)` -- @tparam string location base directory for static files (optional) -- @tparam table plugins list of plugin instances (optional) -- @tparam table logger logger instance (optional) -- @treturn Handler handler ----@param callback fun(request: table, response: table)|nil ----@param location string|nil ----@param plugins table|nil ----@param logger table|nil ----@return Handler function Handler:new(callback, location, plugins, logger) local handler = {} handler.log = logger or require('pegasus.log') @@ -87,7 +75,7 @@ function Handler:new(callback, location, plugins, logger) return result end ---- Allow plugins to alter `Request`/`Response` metatables before use. +-- Allow plugins to alter `Request`/`Response` metatables before use. -- Stops early if a plugin returns a truthy value. function Handler:pluginsAlterRequestResponseMetatable() for _, plugin in ipairs(self.plugins) do @@ -100,12 +88,10 @@ function Handler:pluginsAlterRequestResponseMetatable() end end ---- Run `newConnection` hook across plugins. +-- Run `newConnection` hook across plugins. -- A plugin may wrap or replace the client, or return falsy to abort. -- @tparam table client accepted client socket -- @treturn table|false client or false to stop ----@param client table ----@return table|false function Handler:pluginsNewConnection(client) for _, plugin in ipairs(self.plugins) do if plugin.newConnection then @@ -118,14 +104,11 @@ function Handler:pluginsNewConnection(client) return client end ---- Run `newRequestResponse` hook across plugins. +-- Run `newRequestResponse` hook across plugins. -- Stops early if a plugin returns a truthy value. -- @tparam table request -- @tparam table response -- @treturn any stop value if any plugin aborts ----@param request table ----@param response table ----@return any function Handler:pluginsNewRequestResponse(request, response) for _, plugin in ipairs(self.plugins) do if plugin.newRequestResponse then @@ -137,14 +120,11 @@ function Handler:pluginsNewRequestResponse(request, response) end end ---- Run `beforeProcess` hook across plugins. +-- Run `beforeProcess` hook across plugins. -- Stops early if a plugin returns a truthy value. -- @tparam table request -- @tparam table response -- @treturn any stop value if any plugin aborts ----@param request table ----@param response table ----@return any function Handler:pluginsBeforeProcess(request, response) for _, plugin in ipairs(self.plugins) do if plugin.beforeProcess then @@ -156,14 +136,11 @@ function Handler:pluginsBeforeProcess(request, response) end end ---- Run `afterProcess` hook across plugins. +-- Run `afterProcess` hook across plugins. -- Stops early if a plugin returns a truthy value. -- @tparam table request -- @tparam table response -- @treturn any stop value if any plugin aborts ----@param request table ----@param response table ----@return any function Handler:pluginsAfterProcess(request, response) for _, plugin in ipairs(self.plugins) do if plugin.afterProcess then @@ -175,16 +152,12 @@ function Handler:pluginsAfterProcess(request, response) end end ---- Run `processFile` hook across plugins for a given filename. +-- Run `processFile` hook across plugins for a given filename. -- Stops early if a plugin returns a truthy value. -- @tparam table request -- @tparam table response -- @tparam string filename -- @treturn any stop value if any plugin aborts ----@param request table ----@param response table ----@param filename string ----@return any function Handler:pluginsProcessFile(request, response, filename) for _, plugin in ipairs(self.plugins) do if plugin.processFile then @@ -197,17 +170,13 @@ function Handler:pluginsProcessFile(request, response, filename) end end ---- Run the body data through plugins' `processBodyData` filters. +-- Run the body data through plugins' `processBodyData` filters. -- Each plugin receives `(data, stayOpen, request, response)` and returns the -- (possibly) transformed data. The result of one plugin is passed to the next. -- @tparam string data body chunk (may be empty string) -- @tparam boolean stayOpen whether the connection stays open (chunked) -- @tparam table response associated response -- @treturn string transformed data ----@param data string ----@param stayOpen boolean ----@param response table ----@return string function Handler:processBodyData(data, stayOpen, response) local localData = data @@ -227,16 +196,10 @@ end --- Process a single client by creating `Request`/`Response` and running pipeline. -- If the callback does not close the response, a default 404 page is sent. --- -- @tparam string|number port server port --- @tparam table client accepted client socket --- @tparam table server listening server socket --- @treturn[1] boolean|nil false when connection was rejected by a plugin --- @treturn[2] nil normal completion ----@param port string|integer ----@param client table ----@param server table ----@return boolean|nil +-- @tparam socket client accepted client socket +-- @tparam socket server listening server socket +-- @treturn boolean|nil `false` when connection was rejected by a plugin, `nil` on normal completion function Handler:processRequest(port, client, server) client = self:pluginsNewConnection(client) if not client then diff --git a/src/pegasus/init.lua b/src/pegasus/init.lua index a4ffe2f..140e43b 100644 --- a/src/pegasus/init.lua +++ b/src/pegasus/init.lua @@ -1,24 +1,23 @@ ---- Module `pegasus` +--- The Pegasus server main entry point. -- -- Minimal, embeddable HTTP server with a simple plugin system. -- --- Basic usage: --- ```lua --- local Pegasus = require 'pegasus' --- local server = Pegasus:new{ host = '127.0.0.1', port = '8080' } --- server:start(function(request, response) --- response:statusCode(200) --- response:addHeader('Content-Type', 'text/plain') --- response:write('Hello, world!') --- end) --- ``` --- -- Notes: +-- -- - If [LuaLogging](https://keplerproject.github.io/lualogging/) is available, it will be auto-detected and used for logging. -- - Common plugins include `files`, `router`, `compress`, `downloads`, and `tls`. --- - `start` runs a blocking accept loop; run in a dedicated OS thread/process if you need concurrency. +-- - See `pegasus.handler` for the request/response lifecycle and plugin hooks. +-- +-- Example: -- --- @module pegasus +-- local Pegasus = require 'pegasus' +-- local server = Pegasus:new{ host = '127.0.0.1', port = '8080' } +-- server:start(function(request, response) +-- response:statusCode(200) +-- response:addHeader('Content-Type', 'text/plain') +-- response:write('Hello, world!') +-- end) +-- @classmod pegasus local socket = require 'socket' local Handler = require 'pegasus.handler' @@ -28,46 +27,18 @@ pcall(require, 'logging') --- The Pegasus HTTP server class. -- Instances are created via `Pegasus:new(params)`. --- --- Fields (defaults in parentheses): --- - `host` ("*") bind address, e.g. "127.0.0.1" or "::". --- - `port` ("9090") bind port. --- - `location` ("") base directory for static files/plugins that use the filesystem. --- - `plugins` ({}) array/table of plugin callables or plugin configurations. --- - `timeout` (1) client socket timeout (seconds, blocking operations). --- - `log` (auto) logger compatible with `pegasus.log` API. Defaults to `require('pegasus.log')` and integrates with LuaLogging when present. --- --- @type Pegasus --- @tfield string host --- @tfield string|number port --- @tfield string location --- @tfield table plugins --- @tfield number timeout --- @tfield table log ----@class Pegasus ----@field host string ----@field port string|integer ----@field location string ----@field plugins table ----@field timeout number ----@field log table local Pegasus = {} Pegasus.__index = Pegasus --- Create a new Pegasus server instance. --- --- Parameters table accepts: --- - `host`: bind address (default "*"). --- - `port`: bind port (default "9090"). --- - `location`: base directory used by some plugins (default ""). --- - `plugins`: list/table of plugins to be applied (default {}). --- - `timeout`: client socket timeout in seconds (default 1). --- - `log`: logger instance; if omitted, `pegasus.log` is used (integrates with LuaLogging when available). --- -- @tparam[opt] table params configuration table --- @treturn Pegasus server ----@param params table|nil ----@return Pegasus +-- @tparam[opt='*'] string params.host bind address +-- @tparam[opt=9090] string|number params.port bind port +-- @tparam[opt=""] string params.location base directory used by some plugins +-- @tparam[opt={}] table params.plugins list of plugins to be applied +-- @tparam[opt=1] number params.timeout client socket timeout in seconds +-- @tparam[opt] logger params.log a LuaLogging compatible logger object. Defaults to module `pegasus.log`. +-- @return Pegasus server function Pegasus:new(params) params = params or {} local server = {} @@ -84,10 +55,12 @@ end --- Start the server accept loop (blocking). -- --- The provided callback is invoked once per incoming HTTP request. +-- Starts the server and handles incoming connections. The provided callback is invoked once per incoming HTTP request. +-- Errors during `socket.bind` will raise; accept errors are logged and the loop continues. -- --- Example: --- ```lua +-- @tparam function callback using signature `stop_further_processing = function(request, response)` +-- @raise on bind failure +-- @usage -- server:start(function(request, response) -- -- inspect `request` (method, path, headers, body, query, etc.) -- -- then write a response @@ -95,13 +68,6 @@ end -- response:addHeader('Content-Type', 'text/plain') -- response:write('OK') -- end) --- ``` --- --- Errors during `socket.bind` will raise; accept errors are logged and the loop continues. --- --- @tparam function callback function(request, response) --- @raise on bind failure ----@param callback fun(request: table, response: table) function Pegasus:start(callback) local handler = Handler:new(callback, self.location, self.plugins, self.log) local server = assert(socket.bind(self.host, self.port)) diff --git a/src/pegasus/log.lua b/src/pegasus/log.lua index 1d66194..80b5c42 100644 --- a/src/pegasus/log.lua +++ b/src/pegasus/log.lua @@ -1,8 +1,8 @@ --- logging - --- returns a LuaLogging compatible logger object. +--- logging. +-- Requiring this module returns a LuaLogging compatible logger object. -- If LuaLogging was already loaded, it returns the defaultlogger, -- otherwise returns a stub. The stub has only no-op functions. +-- @module logging local ll = package.loaded.logging if ll and type(ll) == "table" and ll.defaultLogger and diff --git a/src/pegasus/plugins/compress.lua b/src/pegasus/plugins/compress.lua index 1a38c43..8e686b7 100644 --- a/src/pegasus/plugins/compress.lua +++ b/src/pegasus/plugins/compress.lua @@ -1,18 +1,23 @@ ---- Module `pegasus.plugins.compress` +--- gzip compression for responses. -- -- Response body compressor that applies gzip when the client accepts it -- and compression is beneficial. Works with both `lua-zlib` and `lzlib`. -- -- Behavior: +-- -- - Detects `Accept-Encoding: gzip` on the request (or `Content-Encoding` already set) -- - Sets `Content-Encoding: gzip` when compressing -- - Supports streaming with chunked encoding; maintains an internal zlib stream -- - Leaves content unchanged when compression does not reduce size -- --- Options for `Compress:new{ ... }`: --- - `level`: zlib compression level (defaults to zlib default; use `NO_COMPRESSION` to disable) +-- Levels: +-- +-- - `plugin.NO_COMPRESSION` (0): no compression +-- - `plugin.BEST_SPEED` (1): fastest compression +-- - `plugin.BEST_COMPRESSION` (9): best compression ratio +-- - `plugin.DEFAULT_COMPRESSION` (-1): zlib default -- --- @module pegasus.plugins.compress +-- @classmod pegasus.plugins.compress local ZlibStream = {} do local zlib = require "zlib" @@ -98,10 +103,8 @@ local Compress = {} do --- Create a new `Compress` plugin instance. -- @tparam[opt] table options - -- @tparam[opt] number options.level zlib compression level - -- @treturn table plugin instance - ---@param options table|nil - ---@return Compress + -- @tparam[opt=plugin.DEFAULT_COMPRESSION] number options.level zlib compression level (see constants above) + -- @return plugin instance function Compress:new(options) local compress = {} compress.options = options or {} @@ -123,11 +126,6 @@ local Compress = {} do -- @tparam table request request object -- @tparam table response response object -- @treturn string data possibly compressed - ---@param data string|nil - ---@param stayOpen boolean - ---@param request table - ---@param response table - ---@return string function Compress:processBodyData(data, stayOpen, request, response) local accept_encoding diff --git a/src/pegasus/plugins/downloads.lua b/src/pegasus/plugins/downloads.lua index 4219d94..a70e929 100644 --- a/src/pegasus/plugins/downloads.lua +++ b/src/pegasus/plugins/downloads.lua @@ -1,16 +1,17 @@ ---- Module `pegasus.plugins.downloads` +--- Plugin that sends files downstream as downloads. -- -- A plugin that exposes a virtual directory for file downloads. Matches -- a configurable `prefix` and serves files from a configured `location` -- using `Content-Disposition: attachment`. -- --- @module pegasus.plugins.downloads --- @usage --- local Downloads = require('pegasus.plugins.downloads') --- local plugin = Downloads:new{ location = './public', prefix = 'downloads' } --- -- Add plugin to Pegasus --- -- The plugin only responds to `GET` and `HEAD`. +-- +-- Example: +-- +-- local Downloads = require('pegasus.plugins.downloads') +-- local plugin = Downloads:new{ location = './public', prefix = 'downloads' } +-- -- Add plugin to Pegasus +-- @classmod pegasus.plugins.downloads local Downloads = {} Downloads.__index = Downloads @@ -19,20 +20,13 @@ Downloads.__index = Downloads -- same `location` setting as defined in the `Handler`. The `prefix` is a virtual folder -- that triggers the plugin, but will be removed from the filepath if `stripPrefix` is truthy. -- If `stripPrefix` is falsy, then it should be a real folder. --- @tparam options table the options table with the following fields; --- @tparam[opt="./"] options.location string the path to serve files from. Relative to the working directory. --- @tparam[opt="downloads/"] options.prefix string the path prefix that triggers the plugin --- @tparam options.stripPrefix bool whether to strip the prefix from the file path when looking +-- @tparam table options the options table with the following fields; +-- @tparam[opt="./"] string options.location the path to serve files from. Relative to the working directory. +-- @tparam[opt="downloads/"] string options.prefix the path prefix that triggers the plugin +-- @tparam bool options.stripPrefix whether to strip the prefix from the file path when looking -- for the file in the filesystem. Defaults to `false`, unless `options.prefix` is omitted, -- then it defaults to `true`. -- @return the new plugin ---- @tparam table options options table ---- @tparam[opt="./"] string options.location base directory (relative to cwd) ---- @tparam[opt="downloads/"] string options.prefix path prefix triggering the plugin ---- @tparam[opt] boolean options.stripPrefix whether to strip the prefix from the filesystem path ---- @treturn table plugin instance ----@param options table|nil ----@return Downloads function Downloads:new(options) options = options or {} local plugin = {} @@ -74,12 +68,9 @@ function Downloads:new(options) end --- Handle a new request/response pair; serve a download when the path matches. --- @tparam table request --- @tparam table response --- @treturn boolean stop whether request handling should stop ----@param request table ----@param response table ----@return boolean +-- @tparam pegasus.request request +-- @tparam pegasus.response response +-- @treturn boolean stop returns true if the plugin found and wrote the file to the response. function Downloads:newRequestResponse(request, response) local stop = false diff --git a/src/pegasus/plugins/files.lua b/src/pegasus/plugins/files.lua index df67291..bb50151 100644 --- a/src/pegasus/plugins/files.lua +++ b/src/pegasus/plugins/files.lua @@ -1,9 +1,8 @@ ---- Module `pegasus.plugins.files` +--- Plugin that serves static content. -- -- A plugin that serves static content from a folder and optionally redirects --- `/` to a configured default file. --- --- @module pegasus.plugins.files +-- `"/"` to a configured default file. +-- @classmod pegasus.plugins.files local mimetypes = require 'mimetypes' @@ -14,13 +13,11 @@ Files.__index = Files --- Creates a new plugin instance. -- The plugin will only respond to `GET` requests. The files will be served from the -- `location` folder. --- @tparam options table the options table with the following fields; --- @tparam[opt="./"] options.location string the path to serve files from. Relative to the working directory. --- @tparam[opt="index.html"] options.default string filename to serve for top-level without path. Use an empty +-- @tparam table options the options table with the following fields; +-- @tparam[opt="./"] string options.location the path to serve files from. Relative to the working directory. +-- @tparam[opt="index.html"] string options.default filename to serve for top-level without path. Use an empty -- string to have none. -- @return the new plugin ----@param options table|nil ----@return Files function Files:new(options) options = options or {} local plugin = {} @@ -54,15 +51,9 @@ end --- Handle a new request/response pair; serve static files. --- - Redirects `/` to `options.default` when set --- - Serves `GET`/`HEAD` only --- --- @tparam table request --- @tparam table response --- @treturn boolean stop whether request handling should stop ----@param request table ----@param response table ----@return boolean +-- @tparam pegasus.request request +-- @tparam pegasus.response response +-- @treturn boolean stop returns true if the plugin found and wrote the file to the response. function Files:newRequestResponse(request, response) local stop = false diff --git a/src/pegasus/plugins/router.lua b/src/pegasus/plugins/router.lua index d291287..ecd6b03 100644 --- a/src/pegasus/plugins/router.lua +++ b/src/pegasus/plugins/router.lua @@ -1,10 +1,8 @@ ---- Module `pegasus.plugins.router` +--- A router plugin for handling paths/methods/path-parameters. -- -- A plugin that routes requests based on path and method, with support for -- path parameters and pre/post hooks at both router and path levels. -- --- @module pegasus.plugins.router --- -- Supports path parameters. -- -- The `routes` table to configure the router is a hash-table where the keys are the path, and @@ -21,7 +19,7 @@ -- * the path `preFunction` callback is called when there is a `path` match. It can -- be used to do some validations, like path parameters, etc. It is defined on `path` level. -- --- * the `METHOD` (eg. `GET, `POST`, etc) this callback implements the specific method. It is defined +-- * the `METHOD` (eg. `GET`, `POST`, etc) this callback implements the specific method. It is defined -- once for each supported method on the path. The special case is method "`*`" which is a catch-all. -- The catch-all will be used for any method that doesn't have its own handler defined. -- If omitted, the default catch-all will return a "405 Method Not Allowed" error. @@ -42,79 +40,82 @@ -- Route matching is based on a complete match (not prefix). And the order is based on the number -- of path-parameters defined. Least number of parameters go first, such that static paths have -- precedence over variables. --- @usage --- local routes = { --- preFunction = function(req, resp) --- local stop = false --- -- this gets called before any path specific callback -- --- if some_error then --- resp:writeDefaultErrorMessage(400) --- stop = true --- end --- return stop --- end, +-- Example: +-- +-- local routes = { +-- preFunction = function(req, resp) +-- local stop = false +-- -- this gets called before any path specific callback -- +-- if some_error then +-- resp:writeDefaultErrorMessage(400) +-- stop = true +-- end +-- return stop +-- end, -- --- ["/my/{accountNumber}/{param2}/endpoint"] = { -- define path parameters -- --- preFunction = function(req, resp) --- local stop = false --- -- this gets called before any method specific callback, --- -- but after the path-preFunction --- return stop --- end, +-- ["/my/{accountNumber}/{param2}/endpoint"] = { -- define path parameters -- --- GET = function(req, resp) --- local stop = false --- -- this implements the main GET logic --- return stop --- end, +-- preFunction = function(req, resp) +-- local stop = false +-- -- this gets called before any method specific callback, +-- -- but after the router-preFunction +-- return stop +-- end, -- --- POST = function(req, resp) --- local stop = false --- -- this implements the main POST logic --- return stop --- end, +-- GET = function(req, resp) +-- local stop = false +-- -- this implements the main GET logic +-- return stop +-- end, -- --- ["*"] = function(req, resp) --- local stop = false --- -- this implements the wildcard, will handle any method except for the --- -- GET/POST ones defined above. +-- POST = function(req, resp) +-- local stop = false +-- -- this implements the main POST logic +-- return stop +-- end, -- --- -- If the wildcard is not defined, then a default one will be added which --- -- only returns a "405 Method Not Allowed" error. --- return stop --- end, +-- ["*"] = function(req, resp) +-- local stop = false +-- -- this implements the wildcard, will handle any method except for the +-- -- GET/POST ones defined above. -- --- postFunction = function(req, resp) --- local stop = false --- -- this gets called before after the method specific (or wildcard) --- -- callback. +-- -- If the wildcard is not defined, then a default one will be added which +-- -- only returns a "405 Method Not Allowed" error. +-- return stop +-- end, -- --- return stop --- end, --- }, +-- postFunction = function(req, resp) +-- local stop = false +-- -- this gets called after the method specific (or wildcard) +-- -- callback. -- --- ["/my/endpoint"] = function(req, resp) --- local stop = false --- -- this is a shortcut to create a wildcard-method, one callback --- -- to handle any method for this path. Identical to: --- -- ["/my/endpoint"] = { ["*"] = function(req, resp) ... end } --- return stop --- end, +-- return stop +-- end, +-- }, -- --- postFunction = function(req, resp) --- local stop = false --- -- this gets called last. --- return stop --- end, --- } +-- ["/my/endpoint"] = function(req, resp) +-- local stop = false +-- -- this is a shortcut to create a wildcard-method, one callback +-- -- to handle any method for this path. Identical to: +-- -- ["/my/endpoint"] = { ["*"] = function(req, resp) ... end } +-- return stop +-- end, -- --- local router = Router:new { --- prefix = "/api/1v0/", --- routes = routes, --- } +-- postFunction = function(req, resp) +-- local stop = false +-- -- this gets called last. +-- return stop +-- end, +-- } +-- +-- local router = Router:new { +-- prefix = "/api/1v0/", +-- routes = routes, +-- } +-- @classmod pegasus.plugins.router @@ -140,7 +141,7 @@ end ---- Router plugin instance. +-- Router plugin instance. -- -- Options passed to `Router:new{ ... }`: -- - `prefix` (string, optional): base path for all routes @@ -148,9 +149,6 @@ end -- -- Methods invoked by the handler: -- - `newRequestResponse(request, response)` --- --- @type Router ----@class Router local Router = {} Router.__index = Router @@ -242,15 +240,13 @@ end --- Creates a new Router plugin instance. --- @tparam options table the options table with the following fields; --- @tparam[opt] options.prefix string the base path for all underlying routes. --- @tparam options.routes table route definitions to be handled by this router plugin instance. --- @tparam[opt] options.errorHandler function an optional error handler function with signature +-- @tparam table options the options table with the following fields; +-- @tparam[opt] string options.prefix the base path for all underlying routes. +-- @tparam table options.routes route definitions to be handled by this router plugin instance. +-- @tparam[opt] function options.errorHandler an optional error handler function with signature -- `message_to_log = function(request, response, errobj)`. The default handler will send a 500 error -- response, and return a stack trace for logging. -- @return the new plugin ----@param options table|nil ----@return Router function Router:new(options) options = options or {} local plugin = {} @@ -315,9 +311,6 @@ end -- @tparam table request -- @tparam table response -- @treturn boolean stop whether request handling should stop ----@param request table ----@param response table ----@return boolean function Router:newRequestResponse(request, response) local errh = function(...) -- error function that injects request/response objects diff --git a/src/pegasus/plugins/tls.lua b/src/pegasus/plugins/tls.lua index 2963223..64c8098 100644 --- a/src/pegasus/plugins/tls.lua +++ b/src/pegasus/plugins/tls.lua @@ -1,14 +1,13 @@ ---- Module `pegasus.plugins.tls` +--- Implements TLS for secure connections. -- -- A plugin that enables TLS (https) for connections using LuaSec. -- Should be the first plugin, since it wraps the client socket and performs -- the TLS handshake before any other plugin or handler accesses the socket. -- --- @module pegasus.plugins.tls --- -- This plugin should not be used with Copas. Since Copas has native TLS support -- and can handle simultaneous `http` and `https` connections. See the Copas example -- to learn how to set that up. +-- @classmod pegasus.plugins.tls local ssl = require("ssl") @@ -18,7 +17,7 @@ TLS.__index = TLS --- Creates a new plugin instance. -- IMPORTANT: this must be the first plugin to execute before the client-socket is accessed! --- @tparam sslparams table the data-structure that contains the properties for the luasec functions. +-- @tparam table sslparams the data-structure that contains the properties for the luasec functions. -- The structure is set up to mimic the LuaSec functions for the handshake. -- @return the new plugin -- @usage @@ -30,8 +29,6 @@ TLS.__index = TLS -- } -- } -- local tls_plugin = require("pegasus.plugins.tls"):new(sslparams) ----@param sslparams table|nil ----@return TLS function TLS:new(sslparams) sslparams = sslparams or {} assert(sslparams.wrap, "'sslparam.wrap' is a required option") @@ -43,12 +40,9 @@ end --- Wrap an accepted client socket and perform the TLS handshake. -- Optionally sets SNI if provided in `sslparams`. --- @tparam table client accepted client socket --- @tparam table handler the Pegasus handler (for logging) --- @treturn table|false wrapped client or false on failure ----@param client table ----@param handler table ----@return table|false +-- @tparam socket client accepted client socket +-- @tparam pegasus.handler handler the Pegasus handler (for logging) +-- @treturn socket|false wrapped client socket or `false` on failure function TLS:newConnection(client, handler) local params = self.sslparams diff --git a/src/pegasus/request.lua b/src/pegasus/request.lua index 111388c..5bbefe5 100644 --- a/src/pegasus/request.lua +++ b/src/pegasus/request.lua @@ -1,25 +1,31 @@ ---- Module `pegasus.request` +--- Request object available in callback handlers. -- --- Parsed HTTP request facade used by your application callback. --- Instances are created internally by Pegasus and passed to --- `server:start(function(request, response) ... end)`. +-- Parsed HTTP request facade used by your application callback. Instances are +-- created internally by Pegasus and passed to the callback provided to `pegasus:start`, +-- and plugin handlers. +-- +-- Fields on the object: +-- +-- * `request.client`: the client socket (wrapped by TLS if TLS plugin is used) +-- * `request.server`: the server socket +-- * `request.log`: the logger object, to enable logging from handlers and plugins +-- * `request.port`: the port on which the request was received +-- * `request.ip`: the IP address of the client, if available (not supported by LuaSec) +-- * `request.response`: the related `pegasus.response` object to write the response to -- -- Quick example: --- ```lua --- server:start(function(req, res) --- local method = req:method() --- local path = req:path() --- local headers = req:headers() --- local qs = req.querystring -- table of query params -- --- if method == 'POST' then --- local form = req:post() -- urlencoded form body, or nil if not POST --- end --- end) --- ``` +-- server:start(function(req, res) +-- local method = req:method() +-- local path = req:path() +-- local headers = req:headers() +-- local qs = req.querystring -- table of query params -- --- @module pegasus.request - +-- if method == 'POST' then +-- local form = req:post() -- urlencoded form body, or nil if not POST +-- end +-- end) +-- @classmod pegasus.request local Response = require 'pegasus.response' local function normalizePath(path) @@ -50,35 +56,7 @@ local function normalizePath(path) return value end ---- The HTTP request object. --- --- Fields: --- - `client`: the underlying accepted socket client. --- - `server`: the listening server socket. --- - `log`: logger compatible with `pegasus.log` API. --- - `port`: server port bound. --- - `ip`: remote peer address if available (may be `nil` under TLS). --- - `querystring`: table of parsed query-string parameters. Values may be a string or a table of strings if repeated. --- - `response`: associated `Response` object, with a back-reference via `response.request`. --- --- Methods of interest: --- - `method()` -> string --- - `path()` -> string --- - `headers()` -> case-insensitive table of headers (values string or table) --- - `receiveBody([size])` -> string|false (chunked read of request body) --- - `post()` -> table|nil (parses `application/x-www-form-urlencoded` body when method is POST) --- --- Internal helpers: `parseFirstLine`, `parseUrlEncoded`. --- --- @type Request ----@class Request ----@field client table ----@field server table ----@field log table ----@field port string|integer ----@field ip string|nil ----@field querystring table ----@field response table + local Request = {} Request.__index = Request Request.PATTERN_METHOD = '^(.-)%s' @@ -89,18 +67,13 @@ Request.PATTERN_PATH ..Request.PATTERN_PROTOCOL) Request.PATTERN_QUERY_STRING = '([^=]*)=([^&]*)&?' Request.PATTERN_HEADER = '([%w-]+):[ \t]*([%w \t%p]*)' ---- Internal: construct a new Request. +-- Internal: construct a new Request. -- -- @tparam string|number port server port -- @tparam table client accepted client socket -- @tparam table server listening server socket -- @tparam table handler internal handler (provides `log` and body processing) -- @treturn Request request ----@param port string|integer ----@param client table ----@param server table ----@param handler table ----@return Request function Request:new(port, client, server, handler) local obj = {} obj.client = client @@ -123,7 +96,7 @@ function Request:new(port, client, server, handler) return setmetatable(obj, self) end ---- Internal: parse the request line and query-string on first access. +-- Internal: parse the request line and query-string on first access. function Request:parseFirstLine() if (self._firstLine ~= nil) then return @@ -162,11 +135,9 @@ function Request:parseFirstLine() end --- Parse `application/x-www-form-urlencoded` data. --- Returns a table where duplicate keys are represented as an array of values. +-- Returns a hash table. Duplicate keys are represented as an array of values. -- @tparam string data -- @treturn table params ----@param data string|nil ----@return table function Request:parseUrlEncoded(data) local output = {} @@ -188,11 +159,10 @@ function Request:parseUrlEncoded(data) return output end ---- Convenience: parse POST body as `application/x-www-form-urlencoded`. +--- parse POST body as `application/x-www-form-urlencoded`. -- Returns `nil` if method is not POST. --- @treturn[1] table parsed form values +-- @treturn[1] table parsed form values, see `parseUrlEncoded` for details on the format of this table. -- @treturn[2] nil when not a POST request ----@return table|nil function Request:post() if self:method() ~= 'POST' then return nil end local data = self:receiveBody() @@ -201,7 +171,6 @@ end --- Request path (normalized), without query-string. -- @treturn string path ----@return string function Request:path() self:parseFirstLine() return self._path @@ -209,7 +178,6 @@ end --- HTTP method. -- @treturn string method ----@return string function Request:method() self:parseFirstLine() return self._method @@ -227,7 +195,6 @@ end -- Returns a case-insensitive table: looking up with any casing works. -- If a header appears multiple times, the value becomes a table of strings. -- @treturn table headers ----@return table function Request:headers() if self._headerParsed then return self._headers @@ -277,8 +244,6 @@ end -- When a socket timeout happens, any partial data is returned. -- @tparam[opt] number size preferred chunk size -- @treturn string|false chunk or false if no more content ----@param size number|nil ----@return string|false function Request:receiveBody(size) if not self._headerParsed then self:headers() diff --git a/src/pegasus/response.lua b/src/pegasus/response.lua index b5ba939..341135b 100644 --- a/src/pegasus/response.lua +++ b/src/pegasus/response.lua @@ -1,19 +1,28 @@ ---- Module `pegasus.response` +--- Response object available in callback handlers. -- -- HTTP response writer used by your application callback. -- Instances are created internally by Pegasus and passed to --- `server:start(function(request, response) ... end)`. +-- the callback provided to `pegasus:start` and plugin handlers. +-- +-- Notes: +-- +-- - When streaming (`stayOpen == true`), Transfer-Encoding: chunked is used. +-- - For HEAD requests, bodies are automatically skipped. +-- +-- Fields on the object: +-- +-- * `response.log`: the logger object, to enable logging from handlers and plugins +-- * `response.request`: the related `pegasus.request` object +-- * `response.status`: the HTTP status code (defaults to 200), use `response:statusCode` to set the status -- -- Quick example: --- ```lua --- server:start(function(req, res) --- res:statusCode(200) --- :contentType('application/json') --- :write('{"ok":true}') --- end) --- ``` -- --- @module pegasus.response +-- server:start(function(req, res) +-- res:statusCode(200) +-- :contentType('application/json') +-- :write('{"ok":true}') +-- end) +-- @classmod pegasus.response local mimetypes = require 'mimetypes' @@ -99,38 +108,14 @@ local DEFAULT_ERROR_MESSAGE = [[ ]] ---- The HTTP response object. --- --- Usage pattern: chainable calls for fluent responses. --- --- Methods of interest: --- - `statusCode(code[, text])` --- - `contentType(value)` / `addHeader(name, value)` / `addHeaders(table)` --- - `write(body[, stayOpen])` (streams when `stayOpen == true`) --- - `close()` (finish chunked stream) --- - `writeFile(path[, contentType])` (200 OK) --- - `sendFile(path)` (attachment) --- - `redirect(location[, temporary])` --- --- Notes: --- - When streaming (`stayOpen == true`), Transfer-Encoding: chunked is used. --- - For HEAD requests, bodies are automatically skipped. --- --- @type Response ----@class Response ----@field status integer ----@field request table + local Response = {} Response.__index = Response ---- Internal: construct a new Response. --- +-- Internal: construct a new Response. -- @tparam table client accepted client socket -- @tparam table writeHandler internal handler (provides `log` and body processing) -- @treturn Response response ----@param client table ----@param writeHandler table ----@return Response function Response:new(client, writeHandler) local newObj = {} newObj.log = writeHandler.log @@ -148,13 +133,10 @@ function Response:new(client, writeHandler) end --- Add a response header. --- Errors if headers were already sent. +-- Errors if headers were already sent downstream. -- @tparam string key --- @tparam string|number|table value --- @treturn Response self ----@param key string ----@param value any ----@return Response +-- @tparam string|number|array value (multiple headers by the same name can be set by passing an array of values) +-- @return The `pegasus.response` object to allow chaining calls. function Response:addHeader(key, value) assert(not self._headersSended, "can't add header, they were already sent") self._headers[key] = value @@ -162,10 +144,8 @@ function Response:addHeader(key, value) end --- Add multiple headers. --- @tparam table params --- @treturn Response self ----@param params table ----@return Response +-- @tparam table params table of key-value pairs to add as headers (see `addHeader` for value types) +-- @return The `pegasus.response` object to allow chaining calls. function Response:addHeaders(params) for key, value in pairs(params) do self:addHeader(key, value) @@ -176,21 +156,16 @@ end --- Set the `Content-Type` header. -- @tparam string value --- @treturn Response self ----@param value string ----@return Response +-- @return The `pegasus.response` object to allow chaining calls. function Response:contentType(value) return self:addHeader('Content-Type', value) end --- Set the HTTP status line. --- Must be called before headers are sent. +-- Must be called before headers are sent downstream. -- @tparam number statusCode -- @tparam[opt] string statusText (defaults to standard text for the code) --- @treturn Response self ----@param statusCode integer ----@param statusText string|nil ----@return Response +-- @return The `pegasus.response` object to allow chaining calls. function Response:statusCode(statusCode, statusText) assert(not self._headersSended, "can't set status code, it was already sent") self.status = statusCode @@ -200,18 +175,20 @@ function Response:statusCode(statusCode, statusText) return self end ---- Skip writing the response body (used for HEAD requests). --- @tparam[opt] boolean skip defaults to true ----@param skip boolean|nil +--- Instruct response to skip writing the response body. +-- This is automatically called by the `pegasus.request` object for HEAD requests. There should be no need +-- to call this manually. +-- @tparam[opt=true] boolean skip +-- @return The `pegasus.response` object to allow chaining calls. function Response:skipBody(skip) if skip == nil then skip = true end self._skipBody = not not skip + return self end ---- Internal: serialize headers. ----@return string +-- Internal: serialize headers. function Response:_getHeaders() local headers = {} @@ -231,10 +208,7 @@ end --- Write a default HTML error body for a given status. -- @tparam number statusCode -- @tparam[opt] string errMessage --- @treturn Response self ----@param statusCode integer ----@param errMessage string|nil ----@return Response +-- @return The `pegasus.response` object to allow chaining calls. function Response:writeDefaultErrorMessage(statusCode, errMessage) self:statusCode(statusCode) local content = string.gsub(DEFAULT_ERROR_MESSAGE, '{{ STATUS_CODE }}', statusCode) @@ -245,8 +219,7 @@ end --- Finish a chunked response and mark as closed. -- Idempotent; safe to call multiple times. --- @treturn Response self ----@return Response +-- @return The `pegasus.response` object to allow chaining calls. function Response:close() if not self.closed then local body = self._writeHandler:processBodyData(nil, true, self) @@ -267,8 +240,7 @@ end --- Send only the headers, without a body. -- Useful for redirects and HEAD responses. --- @treturn Response self ----@return Response +-- @return The `pegasus.response` object to allow chaining calls. function Response:sendOnlyHeaders() self:sendHeaders(false, '') self:write('\r\n') @@ -279,12 +251,9 @@ end --- Send headers if not already sent. -- Adds `Transfer-Encoding: chunked` when `stayOpen == true`; otherwise sets `Content-Length` when body is a string. -- Also sets a default `Date` and `Content-Type` header if not present. --- @tparam boolean stayOpen whether to keep the connection open for chunked streaming +-- @tparam[opt=false] boolean stayOpen whether to keep the connection open for chunked streaming -- @tparam[opt] string body current body chunk (used to set Content-Length) --- @treturn Response self ----@param stayOpen boolean ----@param body string|nil ----@return Response +-- @return The `pegasus.response` object to allow chaining calls. function Response:sendHeaders(stayOpen, body) if self._headersSended then return self @@ -311,14 +280,10 @@ end --- Write response body. -- When `stayOpen == true`, the body is sent as a chunk and the connection remains open. --- When `stayOpen ~= true`, headers are sent with `Content-Length` and the socket is closed afterwards. --- `nil` body is treated as empty string. --- @tparam[opt] string body +-- When `stayOpen ~= true`, headers are sent with `Content-Length` and the client-socket is closed afterwards. +-- @tparam[opt=""] string body -- @tparam[opt] boolean stayOpen --- @treturn Response self ----@param body string|nil ----@param stayOpen boolean|nil ----@return Response +-- @return The `pegasus.response` object to allow chaining calls. function Response:write(body, stayOpen) body = self._writeHandler:processBodyData(body or '', stayOpen, self) self:sendHeaders(stayOpen, body) @@ -354,14 +319,10 @@ local function readfile(filename) end --- Write a file to the response with a 200 status. --- Returns nil+err if file cannot be read. --- @tparam string|file* filename path or legacy file descriptor (deprecated) --- @tparam[opt] string contentType override content type --- @treturn[1] Response self --- @treturn[2] nil,error on failure ----@param filename any ----@param contentType string|nil ----@return Response|nil,any +-- @tparam string filename path to file +-- @tparam[opt] string contentType override content type, if omitted it will be guessed based on the filename extension +-- @return[1] The `pegasus.response` object to allow chaining calls. +-- @return[2] nil + err on failure function Response:writeFile(filename, contentType) if type(filename) ~= "string" then -- deprecated backward compatibility; file is a file-descriptor @@ -385,12 +346,9 @@ function Response:writeFile(filename, contentType) end --- Send a file as an attachment (download). --- Returns nil+err if the file cannot be read. --- @tparam string path filesystem path --- @treturn[1] Response self --- @treturn[2] nil,error on failure ----@param path string ----@return Response|nil,any +-- @tparam string path path to file +-- @return[1] The `pegasus.response` object to allow chaining calls. +-- @return[2] nil + err on failure function Response:sendFile(path) local filename = path:match("[^/]*$") -- only filename, no path self:addHeader('Content-Disposition', 'attachment; filename="' .. filename .. '"') @@ -407,10 +365,7 @@ end --- Redirect to a different URL. -- @tparam string location destination URL -- @tparam[opt] boolean temporary when true uses 302, otherwise 301 --- @treturn Response self ----@param location string ----@param temporary boolean|nil ----@return Response +-- @return The `pegasus.response` object to allow chaining calls. function Response:redirect(location, temporary) self:statusCode(temporary and 302 or 301) self:addHeader('Location', location) From a4dab253b48212cdb78bf9a9f3ee2fd14e8110d2 Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Mon, 30 Mar 2026 13:30:03 +0200 Subject: [PATCH 3/3] chore(docs): add rendered docs --- docs/classes/pegasus.handler.html | 230 ++++++++ docs/classes/pegasus.html | 226 +++++++ docs/classes/pegasus.plugins.compress.html | 210 +++++++ docs/classes/pegasus.plugins.downloads.html | 207 +++++++ docs/classes/pegasus.plugins.files.html | 183 ++++++ docs/classes/pegasus.plugins.router.html | 307 ++++++++++ docs/classes/pegasus.plugins.tls.html | 186 ++++++ docs/classes/pegasus.request.html | 333 +++++++++++ docs/classes/pegasus.response.html | 583 ++++++++++++++++++ docs/examples/app.lua.html | 215 +++++++ docs/examples/app_stream.lua.html | 105 ++++ docs/examples/copas.lua.html | 264 +++++++++ docs/examples/querystring.lua.html | 104 ++++ docs/examples/write.lua.html | 95 +++ docs/index.html | 159 +++++ docs/ldoc.css | 618 ++++++++++++++++++++ docs/modules/logging.html | 92 +++ docs/topics/LICENSE.html | 100 ++++ docs/topics/README.md.html | 315 ++++++++++ 19 files changed, 4532 insertions(+) create mode 100644 docs/classes/pegasus.handler.html create mode 100644 docs/classes/pegasus.html create mode 100644 docs/classes/pegasus.plugins.compress.html create mode 100644 docs/classes/pegasus.plugins.downloads.html create mode 100644 docs/classes/pegasus.plugins.files.html create mode 100644 docs/classes/pegasus.plugins.router.html create mode 100644 docs/classes/pegasus.plugins.tls.html create mode 100644 docs/classes/pegasus.request.html create mode 100644 docs/classes/pegasus.response.html create mode 100644 docs/examples/app.lua.html create mode 100644 docs/examples/app_stream.lua.html create mode 100644 docs/examples/copas.lua.html create mode 100644 docs/examples/querystring.lua.html create mode 100644 docs/examples/write.lua.html create mode 100644 docs/index.html create mode 100644 docs/ldoc.css create mode 100644 docs/modules/logging.html create mode 100644 docs/topics/LICENSE.html create mode 100644 docs/topics/README.md.html diff --git a/docs/classes/pegasus.handler.html b/docs/classes/pegasus.handler.html new file mode 100644 index 0000000..13535eb --- /dev/null +++ b/docs/classes/pegasus.handler.html @@ -0,0 +1,230 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class pegasus.handler

+

Internal handler that drives the request/response cycle.

+

+ + +

Internal orchestrator that wires the server socket to request/response + objects and drives the plugin pipeline.

+ +

Lifecycle for each connection/request:

+ +
    +
  1. pluginsNewConnection(client) can wrap/replace or reject the client
  2. +
  3. Request/Response objects are created
  4. +
  5. pluginsNewRequestResponse(request, response) runs
  6. +
  7. pluginsBeforeProcess(request, response) runs
  8. +
  9. User callback(request, response) is invoked
  10. +
  11. pluginsAfterProcess(request, response) runs
  12. +
  13. If response not closed, a default 404 is written
  14. +
+ +

Plugins may also:

+ +
    +
  • modify Request/Response metatables via alterRequestResponseMetaTable
  • +
  • intercept file processing via processFile
  • +
  • filter/transform streamed body via processBodyData
  • +
+ +

Minimal plugin example:

+ + +
+local MyPlugin = {}
+
+function MyPlugin:new()
+  return setmetatable({}, { __index = self })
+end
+
+function MyPlugin:beforeProcess(req, res)
+  res:addHeader('X-Powered-By', 'Pegasus')
+end
+
+return MyPlugin
+
+ +

+ + +

Functions

+ + + + + + + + + +
Handler:new (callback, location, plugins, logger)Construct a Handler.
Handler:processRequest (port, client, server)Process a single client by creating Request/Response and running pipeline.
+ +
+
+ + +

Functions

+ Methods +
+
+ + Handler:new (callback, location, plugins, logger) +
+
+ Construct a Handler.

+ +

When location is a non-empty string, automatically enables the files + plugin to serve static files from that directory (default index /index.html). + + +

Parameters:

+
    +
  • callback + function + using signature stop_further_processing = function(request, response) +
  • +
  • location + string + base directory for static files (optional) +
  • +
  • plugins + table + list of plugin instances (optional) +
  • +
  • logger + table + logger instance (optional) +
  • +
+ +

Returns:

+
    + + Handler + handler +
+ + + + +
+
+ + Handler:processRequest (port, client, server) +
+
+ Process a single client by creating Request/Response and running pipeline. + If the callback does not close the response, a default 404 page is sent. + + +

Parameters:

+
    +
  • port + string or number + server port +
  • +
  • client + socket + accepted client socket +
  • +
  • server + socket + listening server socket +
  • +
+ +

Returns:

+
    + + boolean or nil + false when connection was rejected by a plugin, nil on normal completion +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/classes/pegasus.html b/docs/classes/pegasus.html new file mode 100644 index 0000000..ed23c62 --- /dev/null +++ b/docs/classes/pegasus.html @@ -0,0 +1,226 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class pegasus

+

The Pegasus server main entry point.

+

+ + +

Minimal, embeddable HTTP server with a simple plugin system.

+ +

Notes:

+ +
    +
  • If LuaLogging is available, it will be auto-detected and used for logging.
  • +
  • Common plugins include files, router, compress, downloads, and tls.
  • +
  • See pegasus.handler for the request/response lifecycle and plugin hooks.
  • +
+ +

Example:

+ + +
+local Pegasus = require 'pegasus'
+local server = Pegasus:new{ host = '127.0.0.1', port = '8080' }
+server:start(function(request, response)
+  response:statusCode(200)
+  response:addHeader('Content-Type', 'text/plain')
+  response:write('Hello, world!')
+end)
+
+ +

+ + +

Functions

+ + + + + + + + + +
Pegasus:new ([params])Create a new Pegasus server instance.
Pegasus:start (callback)Start the server accept loop (blocking).
+ +
+
+ + +

Functions

+ Methods +
+
+ + Pegasus:new ([params]) +
+
+ Create a new Pegasus server instance. + + +

Parameters:

+
    +
  • params configuration table +
      +
    • host + string + bind address + (default '*') +
    • +
    • port + string or number + bind port + (default 9090) +
    • +
    • location + string + base directory used by some plugins + (default "") +
    • +
    • plugins + table + list of plugins to be applied + (default {}) +
    • +
    • timeout + number + client socket timeout in seconds + (default 1) +
    • +
    • log + logger + a LuaLogging compatible logger object. Defaults to module pegasus.log. + (optional) +
    • +
    +
+ +

Returns:

+
    + + Pegasus server +
+ + + + +
+
+ + Pegasus:start (callback) +
+
+ Start the server accept loop (blocking).

+ +

Starts the server and handles incoming connections. The provided callback is invoked once per incoming HTTP request. + Errors during socket.bind will raise; accept errors are logged and the loop continues. + + +

Parameters:

+
    +
  • callback + function + using signature stop_further_processing = function(request, response) +
  • +
+ + +

Raises:

+ on bind failure + + +

Usage:

+
    +
    server:start(function(request, response)
    +  -- inspect request (method, path, headers, body, query, etc.)
    +  -- then write a response
    +  response:statusCode(200)
    +  response:addHeader('Content-Type', 'text/plain')
    +  response:write('OK')
    +end)
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/classes/pegasus.plugins.compress.html b/docs/classes/pegasus.plugins.compress.html new file mode 100644 index 0000000..71c6ce7 --- /dev/null +++ b/docs/classes/pegasus.plugins.compress.html @@ -0,0 +1,210 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class pegasus.plugins.compress

+

gzip compression for responses.

+

+ + +

Response body compressor that applies gzip when the client accepts it + and compression is beneficial. Works with both lua-zlib and lzlib.

+ +

Behavior:

+ +
    +
  • Detects Accept-Encoding: gzip on the request (or Content-Encoding already set)
  • +
  • Sets Content-Encoding: gzip when compressing
  • +
  • Supports streaming with chunked encoding; maintains an internal zlib stream
  • +
  • Leaves content unchanged when compression does not reduce size
  • +
+ +

Levels:

+ +
    +
  • plugin.NO_COMPRESSION (0): no compression
  • +
  • plugin.BEST_SPEED (1): fastest compression
  • +
  • plugin.BEST_COMPRESSION (9): best compression ratio
  • +
  • plugin.DEFAULT_COMPRESSION (-1): zlib default
  • +
+

+ + +

Functions

+ + + + + + + + + +
Compress:new ([options])Create a new Compress plugin instance.
Compress:processBodyData (data, stayOpen, request, response)Compress response body data when appropriate.
+ +
+
+ + +

Functions

+ Methods +
+
+ + Compress:new ([options]) +
+
+ Create a new Compress plugin instance. + + +

Parameters:

+
    +
  • options + + +
      +
    • level + number + zlib compression level (see constants above) + (default plugin.DEFAULT_COMPRESSION) +
    • +
    +
+ +

Returns:

+
    + + plugin instance +
+ + + + +
+
+ + Compress:processBodyData (data, stayOpen, request, response) +
+
+ Compress response body data when appropriate.

+ +

Invoked for every body chunk. When stayOpen is true, keeps an internal + zlib stream and returns the compressed chunk. On the final call with + data == nil, closes the stream and returns any trailer bytes.

+ +

If compression does not reduce size (for non-streaming case), the + original data is returned and no Content-Encoding header is set. + + +

Parameters:

+
    +
  • data + string or nil + body chunk, or nil to finish streaming +
  • +
  • stayOpen + boolean + whether the response is streaming (chunked) +
  • +
  • request + table + request object +
  • +
  • response + table + response object +
  • +
+ +

Returns:

+
    + + string + data possibly compressed +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/classes/pegasus.plugins.downloads.html b/docs/classes/pegasus.plugins.downloads.html new file mode 100644 index 0000000..18dd67b --- /dev/null +++ b/docs/classes/pegasus.plugins.downloads.html @@ -0,0 +1,207 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class pegasus.plugins.downloads

+

Plugin that sends files downstream as downloads.

+

+ + +

A plugin that exposes a virtual directory for file downloads. Matches + a configurable prefix and serves files from a configured location + using Content-Disposition: attachment.

+ +

The plugin only responds to GET and HEAD.

+ +

Example:

+ + +
+local Downloads = require('pegasus.plugins.downloads')
+local plugin = Downloads:new{ location = './public', prefix = 'downloads' }
+-- Add plugin to Pegasus
+
+ +

+ + +

Functions

+ + + + + + + + + +
Downloads:new (options)Creates a new plugin instance.
Downloads:newRequestResponse (request, response)Handle a new request/response pair; serve a download when the path matches.
+ +
+
+ + +

Functions

+ Methods +
+
+ + Downloads:new (options) +
+
+ Creates a new plugin instance. + The plugin will only respond to GET requests. The files will be served from the + same location setting as defined in the Handler. The prefix is a virtual folder + that triggers the plugin, but will be removed from the filepath if stripPrefix is truthy. + If stripPrefix is falsy, then it should be a real folder. + + +

Parameters:

+
    +
  • options the options table with the following fields; +
      +
    • location + string + the path to serve files from. Relative to the working directory. + (default "./") +
    • +
    • prefix + string + the path prefix that triggers the plugin + (default "downloads/") +
    • +
    • stripPrefix + bool + whether to strip the prefix from the file path when looking + for the file in the filesystem. Defaults to false, unless options.prefix is omitted, + then it defaults to true. +
    • +
    +
+ +

Returns:

+
    + + the new plugin +
+ + + + +
+
+ + Downloads:newRequestResponse (request, response) +
+
+ Handle a new request/response pair; serve a download when the path matches. + + +

Parameters:

+ + +

Returns:

+
    + + boolean + stop returns true if the plugin found and wrote the file to the response. +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/classes/pegasus.plugins.files.html b/docs/classes/pegasus.plugins.files.html new file mode 100644 index 0000000..4129fb2 --- /dev/null +++ b/docs/classes/pegasus.plugins.files.html @@ -0,0 +1,183 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class pegasus.plugins.files

+

Plugin that serves static content.

+

A plugin that serves static content from a folder and optionally redirects + "/" to a configured default file.

+ + +

Functions

+ + + + + + + + + +
Files:new (options)Creates a new plugin instance.
Files:newRequestResponse (request, response)Handle a new request/response pair; serve static files.
+ +
+
+ + +

Functions

+ Methods +
+
+ + Files:new (options) +
+
+ Creates a new plugin instance. + The plugin will only respond to GET requests. The files will be served from the + location folder. + + +

Parameters:

+
    +
  • options the options table with the following fields; +
      +
    • location + string + the path to serve files from. Relative to the working directory. + (default "./") +
    • +
    • default + string + filename to serve for top-level without path. Use an empty + string to have none. + (default "index.html") +
    • +
    +
+ +

Returns:

+
    + + the new plugin +
+ + + + +
+
+ + Files:newRequestResponse (request, response) +
+
+ Handle a new request/response pair; serve static files. + + +

Parameters:

+ + +

Returns:

+
    + + boolean + stop returns true if the plugin found and wrote the file to the response. +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/classes/pegasus.plugins.router.html b/docs/classes/pegasus.plugins.router.html new file mode 100644 index 0000000..d06721c --- /dev/null +++ b/docs/classes/pegasus.plugins.router.html @@ -0,0 +1,307 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class pegasus.plugins.router

+

A router plugin for handling paths/methods/path-parameters.

+

+ + +

A plugin that routes requests based on path and method, with support for + path parameters and pre/post hooks at both router and path levels.

+ +

Supports path parameters.

+ +

The routes table to configure the router is a hash-table where the keys are the path, and + the value is another hash-table. The second hash-table has the method as the key, and the + callbacks as the values. + Both hash-tables can have "preFunction" and "postFunction" entries, + which should have callbacks as values.

+ +

There are 5 callbacks (called in this order);

+ +
    +
  • the router preFunction callback is called first when there is a prefix match. It can + be used to do some validations, like path parameters, etc. It is defined on router level.

  • +
  • the path preFunction callback is called when there is a path match. It can + be used to do some validations, like path parameters, etc. It is defined on path level.

  • +
  • the METHOD (eg. GET, POST, etc) this callback implements the specific method. It is defined + once for each supported method on the path. The special case is method "*" which is a catch-all. + The catch-all will be used for any method that doesn't have its own handler defined. + If omitted, the default catch-all will return a "405 Method Not Allowed" error.

  • +
  • the path postFunction is called after the METHOD callback. This one is defined on path level.

  • +
  • the router postFunction is called last. This one is defined on router level.

  • +
+ +

The callbacks have the following function signature; stop = function(request, response). + If stop is truthy, request handling is terminated, no further callbacks will be called.

+ +

Path parameters can be defined in the path in curly braces; "{variableName}", and they will match + a single path segment. The values will be made available on the Request object as + request.pathParameters.variableName.

+ +

The API sub-path (without the prefix) is available as on the Request object as request.routerPath.

+ +

Route matching is based on a complete match (not prefix). And the order is based on the number + of path-parameters defined. Least number of parameters go first, such that static paths have + precedence over variables.

+ +

Example:

+ + +
+local routes = {
+  preFunction = function(req, resp)
+    local stop = false
+    -- this gets called before any path specific callback
+
+    if some_error then
+      resp:writeDefaultErrorMessage(400)
+      stop = true
+    end
+    return stop
+  end,
+
+
+  ["/my/{accountNumber}/{param2}/endpoint"] = { -- define path parameters
+
+    preFunction = function(req, resp)
+      local stop = false
+      -- this gets called before any method specific callback,
+      -- but after the router-preFunction
+      return stop
+    end,
+
+    GET = function(req, resp)
+      local stop = false
+      -- this implements the main GET logic
+      return stop
+    end,
+
+    POST = function(req, resp)
+      local stop = false
+      -- this implements the main POST logic
+      return stop
+    end,
+
+    ["*"] = function(req, resp)
+      local stop = false
+      -- this implements the wildcard, will handle any method except for the
+      -- GET/POST ones defined above.
+
+      -- If the wildcard is not defined, then a default one will be added which
+      -- only returns a "405 Method Not Allowed" error.
+      return stop
+    end,
+
+    postFunction = function(req, resp)
+      local stop = false
+      -- this gets called after the method specific (or wildcard)
+      -- callback.
+
+      return stop
+    end,
+  },
+
+  ["/my/endpoint"] = function(req, resp)
+    local stop = false
+    -- this is a shortcut to create a wildcard-method, one callback
+    -- to handle any method for this path. Identical to:
+    -- ["/my/endpoint"] = { ["*"] = function(req, resp) ... end }
+    return stop
+  end,
+
+  postFunction = function(req, resp)
+    local stop = false
+    -- this gets called last.
+    return stop
+  end,
+}
+
+local router = Router:new {
+  prefix = "/api/1v0/",
+  routes = routes,
+}
+
+ +

+ + +

Functions

+ + + + + + + + + +
Router:new (options)Creates a new Router plugin instance.
Router:newRequestResponse (request, response)Route the request to the matching path/method callback.
+ +
+
+ + +

Functions

+ Methods +
+
+ + Router:new (options) +
+
+ Creates a new Router plugin instance. + + +

Parameters:

+
    +
  • options the options table with the following fields; +
      +
    • prefix + string + the base path for all underlying routes. + (optional) +
    • +
    • routes + table + route definitions to be handled by this router plugin instance. +
    • +
    • errorHandler + function + an optional error handler function with signature + message_to_log = function(request, response, errobj). The default handler will send a 500 error + response, and return a stack trace for logging. + (optional) +
    • +
    +
+ +

Returns:

+
    + + the new plugin +
+ + + + +
+
+ + Router:newRequestResponse (request, response) +
+
+ Route the request to the matching path/method callback. + Populates request.pathParameters and request.routerPath upon match. + Executes callbacks in order: router pre, path pre, method, path post, router post. + + +

Parameters:

+
    +
  • request + table + + + +
  • +
  • response + table + + + +
  • +
+ +

Returns:

+
    + + boolean + stop whether request handling should stop +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/classes/pegasus.plugins.tls.html b/docs/classes/pegasus.plugins.tls.html new file mode 100644 index 0000000..944f0c5 --- /dev/null +++ b/docs/classes/pegasus.plugins.tls.html @@ -0,0 +1,186 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class pegasus.plugins.tls

+

Implements TLS for secure connections.

+

A plugin that enables TLS (https) for connections using LuaSec. + Should be the first plugin, since it wraps the client socket and performs + the TLS handshake before any other plugin or handler accesses the socket.

+ +

This plugin should not be used with Copas. Since Copas has native TLS support + and can handle simultaneous http and https connections. See the Copas example + to learn how to set that up.

+ + +

Functions

+ + + + + + + + + +
TLS:new (sslparams)Creates a new plugin instance.
TLS:newConnection (client, handler)Wrap an accepted client socket and perform the TLS handshake.
+ +
+
+ + +

Functions

+ Methods +
+
+ + TLS:new (sslparams) +
+
+ Creates a new plugin instance. + IMPORTANT: this must be the first plugin to execute before the client-socket is accessed! + + +

Parameters:

+
    +
  • sslparams + table + the data-structure that contains the properties for the luasec functions. + The structure is set up to mimic the LuaSec functions for the handshake. +
  • +
+ +

Returns:

+
    + + the new plugin +
+ + + +

Usage:

+
    +
    local sslparams = {
    +  wrap = table | context,    -- parameter to LuaSec 'wrap()'
    +  sni = {                    -- parameters to LuaSec 'sni()'
    +    names = string | table   --   1st parameter
    +    strict = bool            --   2nd parameter
    +  }
    +}
    +local tls_plugin = require("pegasus.plugins.tls"):new(sslparams)
    +
+ +
+
+ + TLS:newConnection (client, handler) +
+
+ Wrap an accepted client socket and perform the TLS handshake. + Optionally sets SNI if provided in sslparams. + + +

Parameters:

+
    +
  • client + socket + accepted client socket +
  • +
  • handler + pegasus.handler + the Pegasus handler (for logging) +
  • +
+ +

Returns:

+
    + + socket or false + wrapped client socket or false on failure +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/classes/pegasus.request.html b/docs/classes/pegasus.request.html new file mode 100644 index 0000000..b7c3de3 --- /dev/null +++ b/docs/classes/pegasus.request.html @@ -0,0 +1,333 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class pegasus.request

+

Request object available in callback handlers.

+

+ + +

Parsed HTTP request facade used by your application callback. Instances are + created internally by Pegasus and passed to the callback provided to pegasus:start, + and plugin handlers.

+ +

Fields on the object:

+ +
    +
  • request.client: the client socket (wrapped by TLS if TLS plugin is used)
  • +
  • request.server: the server socket
  • +
  • request.log: the logger object, to enable logging from handlers and plugins
  • +
  • request.port: the port on which the request was received
  • +
  • request.ip: the IP address of the client, if available (not supported by LuaSec)
  • +
  • request.response: the related pegasus.response object to write the response to
  • +
+ +

Quick example:

+ + +
+server:start(function(req, res)
+  local method = req:method()
+  local path = req:path()
+  local headers = req:headers()
+  local qs = req.querystring -- table of query params
+
+  if method == 'POST' then
+    local form = req:post() -- urlencoded form body, or nil if not POST
+  end
+end)
+
+ +

+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Request:headers ()Request headers.
Request:method ()HTTP method.
Request:parseUrlEncoded (data)Parse application/x-www-form-urlencoded data.
Request:path ()Request path (normalized), without query-string.
Request:post ()parse POST body as application/x-www-form-urlencoded.
Request:query ()Query-string parameters.
Request:receiveBody ([size])Receive a chunk of the request body.
+ +
+
+ + +

Functions

+ Methods +
+
+ + Request:headers () +
+
+ Request headers. + Returns a case-insensitive table: looking up with any casing works. + If a header appears multiple times, the value becomes a table of strings. + + + +

Returns:

+
    + + table + headers +
+ + + + +
+
+ + Request:method () +
+
+ HTTP method. + + + +

Returns:

+
    + + string + method +
+ + + + +
+
+ + Request:parseUrlEncoded (data) +
+
+ Parse application/x-www-form-urlencoded data. + Returns a hash table. Duplicate keys are represented as an array of values. + + +

Parameters:

+ + +

Returns:

+
    + + table + params +
+ + + + +
+
+ + Request:path () +
+
+ Request path (normalized), without query-string. + + + +

Returns:

+
    + + string + path +
+ + + + +
+
+ + Request:post () +
+
+ parse POST body as application/x-www-form-urlencoded. + Returns nil if method is not POST. + + + +

Returns:

+
    + + table + parsed form values, see parseUrlEncoded for details on the format of this table. +
+

Or

+
    + + nil + when not a POST request +
+ + + + +
+
+ + Request:query () +
+
+ Query-string parameters. + Returns a hash table. Duplicate keys are represented as an array of values. + + + +

Returns:

+
    + + table + querystring +
+ + + + +
+
+ + Request:receiveBody ([size]) +
+
+ Receive a chunk of the request body. + Ensures headers are parsed. Returns false when there is no body or it is fully consumed. + When a socket timeout happens, any partial data is returned. + + +

Parameters:

+
    +
  • size + number + preferred chunk size + (optional) +
  • +
+ +

Returns:

+
    + + string or false + chunk or false if no more content +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/classes/pegasus.response.html b/docs/classes/pegasus.response.html new file mode 100644 index 0000000..82e8fed --- /dev/null +++ b/docs/classes/pegasus.response.html @@ -0,0 +1,583 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class pegasus.response

+

Response object available in callback handlers.

+

+ + +

HTTP response writer used by your application callback. + Instances are created internally by Pegasus and passed to + the callback provided to pegasus:start and plugin handlers.

+ +

Notes:

+ +
    +
  • When streaming (stayOpen == true), Transfer-Encoding: chunked is used.
  • +
  • For HEAD requests, bodies are automatically skipped.
  • +
+ +

Fields on the object:

+ +
    +
  • response.log: the logger object, to enable logging from handlers and plugins
  • +
  • response.request: the related pegasus.request object
  • +
  • response.status: the HTTP status code (defaults to 200), use response:statusCode to set the status
  • +
+ +

Quick example:

+ + +
+server:start(function(req, res)
+  res:statusCode(200)
+     :contentType('application/json')
+     :write('{"ok":true}')
+end)
+
+ +

+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Response:addHeader (key, value)Add a response header.
Response:addHeaders (params)Add multiple headers.
Response:close ()Finish a chunked response and mark as closed.
Response:contentType (value)Set the Content-Type header.
Response:redirect (location[, temporary])Redirect to a different URL.
Response:sendFile (path)Send a file as an attachment (download).
Response:sendHeaders ([stayOpen=false[, body]])Send headers if not already sent.
Response:sendOnlyHeaders ()Send only the headers, without a body.
Response:skipBody ([skip=true])Instruct response to skip writing the response body.
Response:statusCode (statusCode[, statusText])Set the HTTP status line.
Response:write ([body=""[, stayOpen]])Write response body.
Response:writeDefaultErrorMessage (statusCode[, errMessage])Write a default HTML error body for a given status.
Response:writeFile (filename[, contentType])Write a file to the response with a 200 status.
+ +
+
+ + +

Functions

+ Methods +
+
+ + Response:addHeader (key, value) +
+
+ Add a response header. + Errors if headers were already sent downstream. + + +

Parameters:

+
    +
  • key + string + + + +
  • +
  • value + string, number or array + (multiple headers by the same name can be set by passing an array of values) +
  • +
+ +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+ + + + +
+
+ + Response:addHeaders (params) +
+
+ Add multiple headers. + + +

Parameters:

+
    +
  • params + table + table of key-value pairs to add as headers (see addHeader for value types) +
  • +
+ +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+ + + + +
+
+ + Response:close () +
+
+ Finish a chunked response and mark as closed. + Idempotent; safe to call multiple times. + + + +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+ + + + +
+
+ + Response:contentType (value) +
+
+ Set the Content-Type header. + + +

Parameters:

+ + +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+ + + + +
+
+ + Response:redirect (location[, temporary]) +
+
+ Redirect to a different URL. + + +

Parameters:

+
    +
  • location + string + destination URL +
  • +
  • temporary + boolean + when true uses 302, otherwise 301 + (optional) +
  • +
+ +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+ + + + +
+
+ + Response:sendFile (path) +
+
+ Send a file as an attachment (download). + + +

Parameters:

+
    +
  • path + string + path to file +
  • +
+ +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+

Or

+
    + + nil + err on failure +
+ + + + +
+
+ + Response:sendHeaders ([stayOpen=false[, body]]) +
+
+ Send headers if not already sent. + Adds Transfer-Encoding: chunked when stayOpen == true; otherwise sets Content-Length when body is a string. + Also sets a default Date and Content-Type header if not present. + + +

Parameters:

+
    +
  • stayOpen + boolean + whether to keep the connection open for chunked streaming + (default false) +
  • +
  • body + string + current body chunk (used to set Content-Length) + (optional) +
  • +
+ +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+ + + + +
+
+ + Response:sendOnlyHeaders () +
+
+ Send only the headers, without a body. + Useful for redirects and HEAD responses. + + + +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+ + + + +
+
+ + Response:skipBody ([skip=true]) +
+
+ Instruct response to skip writing the response body. + This is automatically called by the pegasus.request object for HEAD requests. There should be no need + to call this manually. + + +

Parameters:

+
    +
  • skip + boolean + + + + (default true) +
  • +
+ +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+ + + + +
+
+ + Response:statusCode (statusCode[, statusText]) +
+
+ Set the HTTP status line. + Must be called before headers are sent downstream. + + +

Parameters:

+
    +
  • statusCode + number + + + +
  • +
  • statusText + string + (defaults to standard text for the code) + (optional) +
  • +
+ +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+ + + + +
+
+ + Response:write ([body=""[, stayOpen]]) +
+
+ Write response body. + When stayOpen == true, the body is sent as a chunk and the connection remains open. + When stayOpen ~= true, headers are sent with Content-Length and the client-socket is closed afterwards. + + +

Parameters:

+
    +
  • body + string + + + + (default "") +
  • +
  • stayOpen + boolean + + + + (optional) +
  • +
+ +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+ + + + +
+
+ + Response:writeDefaultErrorMessage (statusCode[, errMessage]) +
+
+ Write a default HTML error body for a given status. + + +

Parameters:

+
    +
  • statusCode + number + + + +
  • +
  • errMessage + string + + + + (optional) +
  • +
+ +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+ + + + +
+
+ + Response:writeFile (filename[, contentType]) +
+
+ Write a file to the response with a 200 status. + + +

Parameters:

+
    +
  • filename + string + path to file +
  • +
  • contentType + string + override content type, if omitted it will be guessed based on the filename extension + (optional) +
  • +
+ +

Returns:

+
    + + The pegasus.response object to allow chaining calls. +
+

Or

+
    + + nil + err on failure +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/examples/app.lua.html b/docs/examples/app.lua.html new file mode 100644 index 0000000..1490753 --- /dev/null +++ b/docs/examples/app.lua.html @@ -0,0 +1,215 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

app.lua

+
+-- setup path to find the project source files of Pegasus
+package.path = './src/?.lua;./src/?/init.lua;' .. package.path
+
+-- For this example to work with the https version, you need LuaSec
+-- to be installed, and you need to generate the test certificates from
+-- its examples. Copy the 'A' certificates into this example directory
+-- to make it work.
+-- Then uncomment the TLS plugin section below.
+-- Additionally you need lua-cjson to be installed.
+
+local Pegasus = require 'pegasus'
+local Compress = require 'pegasus.plugins.compress'
+local Downloads = require 'pegasus.plugins.downloads'
+local Files = require 'pegasus.plugins.files'
+local Router = require 'pegasus.plugins.router'
+local json = require 'cjson.safe'
+-- local TLS = require 'pegasus.plugins.tls'
+
+
+-- example data for the "router" plugin
+local routes do
+  local testData = {
+    Jane = { firstName = "Jane", lastName = "Doe", age = 25 },
+    John = { firstName = "John", lastName = "Doe", age = 30 },
+  }
+
+  routes = {
+    -- router-level preFunction runs before the method prefunction and callback
+    preFunction = function(req, resp)
+      local stop = false
+      local headers = req:headers()
+      local accept = (headers.accept or "*/*"):lower()
+      if not accept:find("application/json", 1, true) and
+         not accept:find("application/*", 1, true) and
+         not accept:find("*/*", 1, true) then
+
+        resp:writeDefaultErrorMessage(406, "This API only produces 'application/json'")
+        stop = true
+      end
+      return stop
+    end,
+
+    ["/people"] = {
+      GET = function(req, resp)
+        resp:statusCode(200)
+        resp:addHeader("Content-Type", "application/json")
+        resp:write(json.encode(testData))
+      end,
+    },
+
+    ["/people/{name}"] = {
+      -- path-level preFunction runs before the actual method callback
+      preFunction = function(req, resp)
+        local stop = false
+        local name = req.pathParameters.name
+        if not testData[name] then
+          local err = ("'%s' is an unknown person"):format(name)
+          req.log:error(err)
+          resp:writeDefaultErrorMessage(404, err)
+          stop = true
+        end
+        return stop
+      end,
+
+      -- callback per method
+      GET = function(req, resp)
+        resp:statusCode(200)
+        resp:addHeader("Content-Type", "application/json")
+        resp:write(json.encode(testData[req.pathParameters.name]))
+      end,
+
+      -- postFunction runs after the actual method callback
+      postFunction = function(req, resp)
+        local stop = false
+        req.log:debug("served %s's data", req.pathParameters.name)
+        return stop
+      end,
+    }
+  }
+end
+
+
+local server = Pegasus:new({
+  port = '9090',
+  plugins = {
+    -- TLS:new {  -- the tls specific configuration
+    --   wrap = {
+    --     mode = "server",
+    --     protocol = "any",
+    --     key = "./example/serverAkey.pem",
+    --     certificate = "./example/serverA.pem",
+    --     cafile = "./example/rootA.pem",
+    --     verify = {"none"},
+    --     options = {"all", "no_sslv2", "no_sslv3", "no_tlsv1"},
+    --   },
+    --   sni = nil,
+    -- },
+
+    Downloads:new {
+      location = '/example/root/',
+      prefix = 'downloads',
+      stripPrefix = true,
+    },
+
+    Files:new {
+      location = '/example/root/',
+    },
+
+    Router:new {
+      prefix = "/api/1v0/",
+      routes = routes,
+    },
+
+    Compress:new(),
+  }
+})
+
+server:start(function(req, resp)
+  local stop = false
+
+  local path = req:path()
+  if req:method() ~= "POST" or path ~= "/index.html" then
+    return stop
+  end
+
+  local data = req:post()
+  if data then
+    print("Name: ", data.name)
+    print("Age: ", data.age)
+  end
+  stop = not not resp:writeFile("./example/root" .. path)
+  return stop
+end)
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/examples/app_stream.lua.html b/docs/examples/app_stream.lua.html new file mode 100644 index 0000000..e27ca74 --- /dev/null +++ b/docs/examples/app_stream.lua.html @@ -0,0 +1,105 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

app_stream.lua

+
+-- setup path to find the project source files of Pegasus
+package.path = "./src/?.lua;./src/?/init.lua;"..package.path
+
+local Pegasus = require 'pegasus'
+local socket = require 'socket'
+
+local server = Pegasus:new({
+  port='9090'
+})
+local function sleep(sec)
+  socket.select(nil, nil, sec)
+end
+
+server:start(function(req, res)
+  res:write('a', true)
+  sleep(3)
+  res:write('b', true)
+  sleep(3)
+  res:write('c', true)
+
+  -- return a truthy value to indicate the request was handled, no further handling needed
+  return res:close()
+end)
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/examples/copas.lua.html b/docs/examples/copas.lua.html new file mode 100644 index 0000000..4b42b37 --- /dev/null +++ b/docs/examples/copas.lua.html @@ -0,0 +1,264 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

copas.lua

+
+-- setup path to find the project source files of Pegasus
+package.path = "./src/?.lua;./src/?/init.lua;"..package.path
+
+
+-- Example that uses Copas as a socket scheduler, allowing multiple
+-- servers to work in parallel.
+-- For this example to work with the https version, you need LuaSec
+-- to be installed, and you need to generate the test certificates from
+-- its examples. Copy the 'A' certificates into this example directory
+-- to make it work.
+-- Additionally you need lua-cjson to be installed.
+
+-- require lualogging if available, "pegasus.log" will automatically pick it up
+pcall(require, 'logging')
+
+local Handler = require 'pegasus.handler'
+local copas = require('copas')
+local socket = require('socket')
+local Downloads = require 'pegasus.plugins.downloads'
+local Files = require 'pegasus.plugins.files'
+local Router = require 'pegasus.plugins.router'
+local Compress = require 'pegasus.plugins.compress'
+local json = require 'cjson.safe'
+
+
+--- Creates a new server within the Copas scheduler.
+-- @tparam table opts options table.
+-- @tparam[opt='*'] string opts.interface the interface to listen on, or '*' for all.
+-- @tparam string          opts.port the port number to listen on.
+-- @tparam[opt] table      opts.sslparams the tls based parameters, see the Copas documentation.
+--                         If not provided, then the connection will be accepted as a plain one.
+-- @tparam[opt] table      opts.plugins the plugins to use
+-- @tparam[opt] function   opts.callback the callback function to handle requests
+-- @tparam[opt] string     opts.location the file-path from where to serve files
+-- @tparam[opt] logger     opts.log the LuaLogging logger to use (defaults to LuaLogging default logger)
+-- @return the server-socket on success, or nil+err on failure
+local function newPegasusServer(opts)
+  opts = opts or {}
+  assert(opts.port, "option 'port' must be provided")
+
+  local server_sock, err = socket.bind(opts.interface or '*', opts.port)
+  if not server_sock then
+    return nil, "failed to create server socket; ".. tostring(err)
+  end
+
+  local server_ip, server_port = server_sock:getsockname()
+  if not server_ip then
+    return nil, "failed to get server socket name; "..tostring(server_port)
+  else
+    if server_ip == "0.0.0.0" then
+      server_ip = "localhost"
+    end
+  end
+
+  local hdlr = Handler:new(opts.callback, opts.location, opts.plugins, opts.log)
+
+  copas.addserver(server_sock, copas.handler(function(client_sock)
+    hdlr:processRequest(server_port, client_sock)
+  end, opts.sslparams))
+
+  hdlr.log:info('Pegasus is up on %s://%s:%s', opts.sslparams and "https" or "http", server_ip, server_port)
+  return server_sock
+end
+
+
+-- example data for the "router" plugin
+local routes do
+  local testData = {
+    Jane = { firstName = "Jane", lastName = "Doe", age = 25 },
+    John = { firstName = "John", lastName = "Doe", age = 30 },
+  }
+
+  routes = {
+    -- router-level preFunction runs before the method prefunction and callback
+    preFunction = function(req, resp)
+      local stop = false
+      local headers = req:headers()
+      local accept = (headers.accept or "*/*"):lower()
+      if not accept:find("application/json", 1, true) and
+         not accept:find("application/*", 1, true) and
+         not accept:find("*/*", 1, true) then
+
+        resp:writeDefaultErrorMessage(406, "This API only produces 'application/json'")
+        stop = true
+      end
+      return stop
+    end,
+
+    ["/people"] = {
+      GET = function(req, resp)
+        resp:statusCode(200)
+        resp:addHeader("Content-Type", "application/json")
+        resp:write(json.encode(testData))
+      end,
+    },
+
+    ["/people/{name}"] = {
+      -- path-level preFunction runs before the actual method callback
+      preFunction = function(req, resp)
+        local stop = false
+        local name = req.pathParameters.name
+        if not testData[name] then
+          local err = ("'%s' is an unknown person"):format(name)
+          req.log:error(err)
+          resp:writeDefaultErrorMessage(404, err)
+          stop = true
+        end
+        return stop
+      end,
+
+      -- callback per method
+      GET = function(req, resp)
+        resp:statusCode(200)
+        resp:addHeader("Content-Type", "application/json")
+        resp:write(json.encode(testData[req.pathParameters.name]))
+      end,
+
+      -- postFunction runs after the actual method callback
+      postFunction = function(req, resp)
+        local stop = false
+        req.log:debug("served %s's data", req.pathParameters.name)
+        return stop
+      end,
+    }
+  }
+end
+
+
+
+-- Create http server
+assert(newPegasusServer{
+  interface = "*",
+  port = "9090",
+  sslparams = nil,
+  location = nil,
+  callback = function(req, resp) -- just redirecting to the https one
+    local host = (req:headers()["Host"] or ""):match("^([^:]+)")
+    resp:redirect("https://" .. host .. ":9091" .. req:path())
+  end,
+  plugins = {},
+})
+
+
+-- Create https server
+assert(newPegasusServer{
+  interface = "*",
+  port = "9091",
+  sslparams = {  -- the tls specific configuration
+    wrap = {
+      mode = "server",
+      protocol = "any",
+      key = "./example/serverAkey.pem",
+      certificate = "./example/serverA.pem",
+      cafile = "./example/rootA.pem",
+      verify = {"none"},
+      options = {"all", "no_sslv2", "no_sslv3", "no_tlsv1"},
+    },
+    sni = nil,
+  },
+
+  plugins = {
+    Downloads:new {
+      location = '/example/root/',
+      prefix = 'downloads',
+      stripPrefix = true,
+    },
+
+    Files:new {
+      location = '/example/root/',
+    },
+
+    Router:new {
+      prefix = "/api/1v0/",
+      routes = routes,
+    },
+
+    Compress:new(),
+  }
+})
+
+-- Start
+copas.loop()
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/examples/querystring.lua.html b/docs/examples/querystring.lua.html new file mode 100644 index 0000000..f467142 --- /dev/null +++ b/docs/examples/querystring.lua.html @@ -0,0 +1,104 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

querystring.lua

+
+-- setup path to find the project source files of Pegasus
+package.path = "./src/?.lua;./src/?/init.lua;"..package.path
+
+local pegasus = require 'pegasus'
+
+local server = pegasus:new({
+  port = '9090',
+})
+
+local printTable = function(table)
+  for k, v in pairs(table) do
+    print(k, '=', v)
+  end
+end
+
+server:start(function (req, res)
+  printTable(req:query())
+  res:addHeader('Content-Type', 'text/html'):write('hello pegasus world!')
+
+  -- return a truthy value to indicate the request was handled, no further handling needed
+  return res:close()
+end)
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/examples/write.lua.html b/docs/examples/write.lua.html new file mode 100644 index 0000000..f6bac84 --- /dev/null +++ b/docs/examples/write.lua.html @@ -0,0 +1,95 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

write.lua

+
+-- setup path to find the project source files of Pegasus
+package.path = "./src/?.lua;./src/?/init.lua;"..package.path
+
+local Pegasus = require 'pegasus'
+
+local server = Pegasus:new()
+
+server:start(function (req, res)
+  res:addHeader('Content-Type', 'text/html'):write('hello pegasus world!')
+
+  -- return a truthy value to indicate the request was handled, no further handling needed
+  return res:close()
+end)
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..979b882 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,159 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ + +

Minimal, embeddable HTTP server with a simple plugin system.

+ +

Modules

+ + + + + +
logginglogging.
+

Classes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
pegasusThe Pegasus server main entry point.
pegasus.handlerInternal handler that drives the request/response cycle.
pegasus.plugins.compressgzip compression for responses.
pegasus.plugins.downloadsPlugin that sends files downstream as downloads.
pegasus.plugins.filesPlugin that serves static content.
pegasus.plugins.routerA router plugin for handling paths/methods/path-parameters.
pegasus.plugins.tlsImplements TLS for secure connections.
pegasus.requestRequest object available in callback handlers.
pegasus.responseResponse object available in callback handlers.
+

Topics

+ + + + + + + + + +
LICENSE
README.md
+

Examples

+ + + + + + + + + + + + + + + + + + + + + +
app.lua
app_stream.lua
copas.lua
querystring.lua
write.lua
+ +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/ldoc.css b/docs/ldoc.css new file mode 100644 index 0000000..df2c98f --- /dev/null +++ b/docs/ldoc.css @@ -0,0 +1,618 @@ +/* Pegasus LDoc — modern documentation theme (CSS only) */ +:root { + --bg-page: #f4f6f9; + --bg-surface: #ffffff; + --bg-sidebar: #fafbfc; + --border: #e2e8f0; + --border-strong: #cbd5e1; + --text: #1e293b; + --text-muted: #64748b; + --accent: #2563eb; + --accent-hover: #1d4ed8; + --accent-soft: rgba(37, 99, 235, 0.08); + --accent-ring: rgba(37, 99, 235, 0.35); + --header-ink: #0f172a; + --radius: 10px; + --radius-sm: 6px; + --shadow: 0 1px 3px rgba(15, 23, 42, 0.06); + --shadow-md: 0 4px 24px rgba(15, 23, 42, 0.08); + --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + --font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Source Code Pro", + Menlo, Consolas, monospace; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + color: var(--text); + font-size: 16px; + line-height: 1.6; + font-family: var(--font-sans); + background: var(--bg-page); + -webkit-font-smoothing: antialiased; +} + +#product { + display: none; +} + +#container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +#main { + display: flex; + flex: 1; + align-items: flex-start; + width: 100%; +} + +a:link, +a:visited { + color: var(--accent); + text-decoration-skip-ink: auto; +} + +a:hover { + color: var(--accent-hover); + text-decoration: underline; + text-underline-offset: 2px; +} + +h1 { + font-size: 1.75rem; + font-weight: 650; + letter-spacing: -0.02em; + color: var(--header-ink); +} + +h2 { + font-size: 1.35rem; + font-weight: 600; + letter-spacing: -0.015em; + color: var(--header-ink); +} + +h3 { + font-size: 1.125rem; + font-weight: 600; + color: var(--header-ink); +} + +h4 { + font-size: 1rem; + font-weight: 600; + color: var(--header-ink); +} + +hr { + height: 1px; + background: var(--border); + border: 0; + margin: 1.25rem 0; +} + +code, +tt { + font-family: var(--font-mono); + font-size: 0.9em; + background: var(--accent-soft); + color: var(--header-ink); + padding: 0.15em 0.4em; + border-radius: 4px; +} + +#content h1 code { + background: rgba(255, 255, 255, 0.15); + color: inherit; + padding: 0.1em 0.35em; +} + +span.parameter { + font-family: var(--font-mono); + font-weight: 600; + color: var(--text-muted); +} + +span.parameter::after { + content: ":"; +} + +span.types::before { + content: "("; +} + +span.types::after { + content: ")"; +} + +.type { + font-weight: 600; + font-style: italic; + color: var(--text-muted); +} + +p.name { + font-family: var(--font-mono); + font-size: 0.95em; +} + +#navigation { + float: none; + flex: 0 0 min(17rem, 42vw); + max-width: 17rem; + vertical-align: top; + overflow: visible; + background: + linear-gradient(180deg, rgba(37, 99, 235, 0.06), rgba(37, 99, 235, 0)) + 0 0 / 100% 240px no-repeat, + var(--bg-sidebar); + border-right: 1px solid var(--border); + box-shadow: var(--shadow); + position: sticky; + top: 0; + align-self: stretch; + max-height: 100vh; + overflow-y: auto; +} + +#navigation br { + display: none; +} + +#navigation h1 { + margin: 0; + padding: 1.25rem 1.25rem 1rem; + font-size: 1.1rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--header-ink); + background: transparent; + border-bottom: 1px solid var(--border); +} + +#navigation h2 { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); + background: transparent; + border-bottom: none; + padding: 1.25rem 1.25rem 0.5rem; + margin: 0; +} + +#navigation ul + h2 { + border-top: 1px dashed var(--border); + margin-top: 0.25rem; +} + +#content { + float: none; + margin-left: 0; + flex: 1; + min-width: 0; + background: var(--bg-surface); + border-radius: 0; + box-shadow: var(--shadow-md); +} + +#content h1 { + margin: 0; + padding: 1.35rem 1.5rem; + background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%); + color: #f8fafc; + font-weight: 650; + letter-spacing: -0.02em; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +#content h2 { + margin: 0; + padding: 0.85rem 1.5rem; + background: var(--bg-page); + color: var(--header-ink); + font-weight: 600; + font-size: 1.15rem; + border-bottom: 1px solid var(--border); + border-top: 1px solid var(--border); +} + +#content h2:first-of-type { + border-top: none; +} + +#content h2 a { + background: transparent; + color: inherit; + text-decoration: none; +} + +#content h2 a:hover { + color: var(--accent); + text-decoration: none; +} + +#content h3 { + font-style: normal; + margin: 1.5rem 1.5rem 0.5rem; + padding: 0 0 0.5rem; + border-bottom: 2px solid var(--border); + color: var(--header-ink); +} + +#content h4 { + margin: 1.25rem 1.5rem 0.35rem; + padding: 0 0 0.35rem; + border-bottom: 1px solid var(--border); +} + +#content pre { + margin: 1rem 1.5rem; +} + +pre code { + background: transparent; + padding: 0; + color: inherit; + font-size: inherit; + border-radius: 0; +} + +pre { + background: #1a1d26; + color: #e2e8f0; + border-radius: var(--radius); + padding: 1rem 1.15rem; + overflow: auto; + font-family: var(--font-mono); + font-size: 0.875rem; + line-height: 1.55; + border: 1px solid rgba(255, 255, 255, 0.06); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); +} + +#content ul pre.example { + margin-left: 0; +} + +table.index { + width: 100%; +} + +table.index td { + text-align: left; + vertical-align: top; +} + +#navigation ul { + font-size: 0.9375rem; + list-style: none; + margin: 0 0 0.75rem; + padding: 0.25rem 0.75rem 0.75rem; +} + +#navigation li { + text-indent: 0; + display: block; + margin: 0.125rem 0; +} + +#navigation li a { + position: relative; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.7rem; + border-radius: 10px; + color: var(--text); + text-decoration: none; + border: 1px solid transparent; + transition: background-color 0.15s ease, border-color 0.15s ease, + transform 0.05s ease; +} + +#navigation li a:hover { + text-decoration: none; + background: var(--accent-soft); + border-color: rgba(37, 99, 235, 0.18); + transform: translateY(-1px); +} + +#navigation li a:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--accent-ring); +} + +#navigation li li a { + margin: 0; +} + +/* Current page: LDoc renders it as inside the list */ +#navigation li strong { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.7rem; + border-radius: 10px; + border: 1px solid transparent; + background: rgba(37, 99, 235, 0.08); + color: var(--accent-hover); + font-weight: 700; + box-shadow: inset 3px 0 0 rgba(37, 99, 235, 0.55); +} + +#content p { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +#content table { + padding-left: 1.5rem; + padding-right: 1.5rem; + background: transparent; +} + +#content p, +#content table, +#content ol, +#content ul, +#content dl { + max-width: 52rem; +} + +#about { + padding: 1rem 1.5rem; + padding-left: 1.5rem; + background: var(--bg-surface); + border-top: 1px solid var(--border); + color: var(--text-muted); + font-size: 0.875rem; +} + +#about a { + color: var(--accent); +} + +table.module_list, +table.function_list { + border-collapse: separate; + border-spacing: 0; + margin: 1rem 1.5rem; + width: calc(100% - 3rem); + max-width: 52rem; + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + box-shadow: var(--shadow); +} + +table.module_list tr:nth-child(even) td, +table.function_list tr:nth-child(even) td { + background: #f8fafc; +} + +table.module_list td, +table.function_list td { + padding: 0.65rem 1rem; + border: none; + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +table.module_list tr:last-child td, +table.function_list tr:last-child td { + border-bottom: none; +} + +table.module_list td.name, +table.function_list td.name { + background: transparent; + min-width: 200px; + border-right: 1px solid var(--border); + font-family: var(--font-mono); + font-size: 0.9em; +} + +table.module_list td.summary, +table.function_list td.summary { + background: transparent; + width: 100%; + color: var(--text-muted); +} + +dl.function { + margin: 1rem 1.5rem 1.5rem; + border: none; + border-radius: 0; + background: transparent; + box-shadow: none; + overflow: visible; +} + +dl.function dt { + color: var(--accent); + font-family: var(--font-mono); + font-size: 0.9em; + border: 1px solid var(--border); + border-bottom: none; + border-radius: var(--radius) var(--radius) 0 0; + padding: 0.9rem 1.15rem; + background: linear-gradient(180deg, #f8fafc 0%, #fff 100%); + box-shadow: var(--shadow); + margin-top: 1rem; +} + +dl.function dt:first-child { + margin-top: 0; +} + +dl.function dd { + margin: 0; + padding: 0.75rem 1.15rem 1.25rem; + border: 1px solid var(--border); + border-top: none; + border-radius: 0 0 var(--radius) var(--radius); + background: var(--bg-surface); + box-shadow: var(--shadow); + margin-bottom: 1rem; +} + +#content dl.function dd h3 { + margin: 1.05rem 0 0.55rem; + padding: 0.85rem 0 0; + font-size: 0.9rem; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--text-muted); + border-top: 1px solid var(--border); +} + +#content dl.function dd ul, +#content dl.function dd ol { + margin: 0.25rem 0 0.75rem; + padding: 0; + padding-left: 1.25rem; + list-style-type: none; +} + +ul.nowrap { + overflow: auto; + white-space: nowrap; +} + +.section-description { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +ul ul, +ol ul, +ol ol, +ul ol { + margin-top: 0; +} + +a:target + * { + background-color: #fef9c3; + box-shadow: inset 3px 0 0 0 #eab308; + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + padding-left: 0.35rem; + margin-left: -0.35rem; + transition: background-color 0.2s ease; +} + +/* Syntax highlighting (prettify) */ +pre .comment { + color: #94a3b8; + font-style: italic; +} + +pre .constant { + color: #fbbf24; +} + +pre .escape { + color: #f472b6; +} + +pre .keyword { + color: #c084fc; + font-weight: 600; +} + +pre .library { + color: #5eead4; +} + +pre .marker { + color: #1e293b; + background: #fde047; + font-weight: 600; + padding: 0 0.15em; + border-radius: 3px; +} + +pre .string { + color: #86efac; +} + +pre .number { + color: #fdba74; +} + +pre .operator { + color: #93c5fd; + font-weight: 600; +} + +pre .preprocessor, +pre .prepro { + color: #fb923c; +} + +pre .global { + color: #e879f9; +} + +pre .user-keyword { + color: #d8b4fe; +} + +pre .prompt { + color: #4ade80; +} + +pre .url { + color: #7dd3fc; + text-decoration: underline; +} + +pre .function-name { + color: #7dd3fc; +} + +@media (max-width: 768px) { + #main { + flex-direction: column; + } + + #navigation { + position: relative; + max-height: none; + flex: none; + width: 100%; + max-width: none; + border-right: none; + border-bottom: 1px solid var(--border); + } + + #content p, + #content table, + #content ol, + #content ul, + #content dl { + max-width: none; + } + + table.module_list, + table.function_list { + width: calc(100% - 2rem); + margin-left: 1rem; + margin-right: 1rem; + } +} diff --git a/docs/modules/logging.html b/docs/modules/logging.html new file mode 100644 index 0000000..d536082 --- /dev/null +++ b/docs/modules/logging.html @@ -0,0 +1,92 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module logging

+

logging.

+

Requiring this module returns a LuaLogging compatible logger object. + If LuaLogging was already loaded, it returns the defaultlogger, + otherwise returns a stub. The stub has only no-op functions.

+ + + +
+
+ + + + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/topics/LICENSE.html b/docs/topics/LICENSE.html new file mode 100644 index 0000000..79d2227 --- /dev/null +++ b/docs/topics/LICENSE.html @@ -0,0 +1,100 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ + The MIT License (MIT)

+ +

Copyright (c) 2016 Evandro Leopoldino Gonçalves

+ +

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. + +

+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ + diff --git a/docs/topics/README.md.html b/docs/topics/README.md.html new file mode 100644 index 0000000..ccf2fe7 --- /dev/null +++ b/docs/topics/README.md.html @@ -0,0 +1,315 @@ + + + + + Pegasus webserver in Lua + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ + +

pegasus.lua

+ +

An http server to work with web applications written in Lua language check the site.

+ +

Unix build +Lint +HuBoard
+badge +Gitter

+ +

+

Installation

+ +

To install Pegasus.lua, run:

+ + +
+$ luarocks install pegasus
+
+ + +

+

How does it work?

+ +

Follow an example:

+ + +
+local pegasus = require 'pegasus'
+
+local server = pegasus:new({
+  port='9090',
+  location='example/root'
+})
+
+server:start(function (request, response)
+  print "It's running..."
+end)
+
+ + +

Or try the included examples.

+ +

+

Features

+ +
    +
  • Compatible with Linux, Mac and Windows systems
  • +
  • Easy API
  • +
  • Support Lua >= 5.1
  • +
  • Native support for HTTP Streaming, aka chunked responses. Check how it works.
  • +
  • Native plugin to compress responses using the "gzip" method
  • +
+ +

+

API

+ +

Parameters

+ +
    +
  • host:string Host address where the application will run. By default it uses localhost
  • +
  • port:string The port where the application will run. By default it's 9090
  • +
  • location:string Path used by Pegasus to search for the files. By default it's the root
  • +
  • plugins:table List with plugins
  • +
  • timeout:number It's a timeout for estabilishing a connection with the server
  • +
+ +

Request

+ +

Properties

+ +
    +
  • path:string A string with the request path
  • +
  • headers:table A table with all the headers data
  • +
  • method:function The output is the request method as a string ('GET', 'POST', etc)
  • +
  • querystring:string It returns a dictionary with all the GET parameters
  • +
  • ip:string It returns the client's ip
  • +
  • port:number It returns the port where Pegasus is running
  • +
+ +

Response

+ +

Methods

+ +
    +
  • addHeader(string:key, string:value) Adds a new header
  • +
  • addHeaders(table:headers) It adds news headers
  • +
  • statusCode(number:statusCode, string:statusMessage) It adds a Status Code
  • +
  • contentType(string:value) Adds a value to Content-Type field
  • +
  • write(string:body) It creates the body with the value passed as + parameter
  • +
  • writeDefaultErrorMessage(statusCode: string, message:body) It sets an HTTP status code and writes an error message to the response
  • +
  • writeFile(string:file) It creates the body with the content of the + file passed as parameter
  • +
  • post():table It returns a dictionary with all the POST parameters
  • +
  • redirect(location:string, temporary:boolean): Makes an HTTP redirect to a new location. The status code is set to 302 if temporary is true and false otherwise.
  • +
+ + +
+local pegasus = require 'pegasus'
+
+local server = pegasus:new({ port='9090' })
+
+server:start(function (req, rep)
+  rep:addHeader('Date', 'Mon, 15 Jun 2015 14:24:53 GMT'):write('hello pegasus world!')
+  return rep:close()
+end)
+
+ + +

+

Native Plugin

+ +
    +
  • pegasus.plugins.compress
  • +
+ + +
+local Pegasus = require 'pegasus'
+local Compress = require 'pegasus.plugins.compress'
+
+local server = Pegasus:new({
+  plugins = { Compress:new() }
+})
+
+server:start()
+
+ + +
    +
  • pegasus.plugins.downloads
  • +
+ + +
+local Pegasus = require 'pegasus'
+local Downloads = require 'pegasus.plugins.downloads'
+
+local server = Pegasus:new({
+  plugins = {
+    Downloads:new {
+      prefix = "downloads",
+      stripPrefix = true,
+    },
+  }
+})
+
+server:start()
+
+ + + +
    +
  • pegasus.plugins.files
  • +
+ + +
+local Pegasus = require 'pegasus'
+local Files = require 'pegasus.plugins.files'
+
+local server = Pegasus:new({
+  plugins = {
+    Files:new {
+      location = "./",
+      default = "index.html",
+    },
+  }
+})
+
+server:start()
+
+ + +
    +
  • pegasus.plugins.tls
  • +
+ + +
+local Pegasus = require 'pegasus'
+local Tls = require 'pegasus.plugins.tls'
+
+local server = Pegasus:new({
+  plugins = {
+    TLS:new {
+      wrap = {
+        mode = "server",
+        protocol = "any",
+        key = "./serverAkey.pem",
+        certificate = "./serverA.pem",
+        cafile = "./rootA.pem",
+        verify = {"none"},
+        options = {"all", "no_sslv2", "no_sslv3", "no_tlsv1"},
+      },
+      sni = nil,
+    },,
+  }
+})
+
+server:start()
+
+ + +

+

Contributing

+ +

Install Dependencies

+ + +
+$ make install_dependencies
+
+ + +

Running tests

+ + +
+$ make unit_test
+
+ + + + +
+
+
+generated by LDoc 1.5.0 +Last updated 2026-03-30 13:29:33 +
+
+ +