Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions rockspecs/pegasus-1.0.9-0.rockspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
local package_name = "pegasus"
local package_version = "1.0.9"
local rockspec_revision = "0"
local github_account_name = "evandrolg"
local github_repo_name = "pegasus.lua"


package = package_name
version = package_version .. "-" .. rockspec_revision

source = {
url = "git+https://github.com/" .. github_account_name .. "/" .. github_repo_name .. ".git",
branch = (package_version == "dev") and "master" or nil,
tag = (package_version ~= "dev") and ("v" .. package_version) or nil,
}

description = {
summary = 'Pegasus.lua is an http server to work with web applications written in Lua language.',
maintainer = 'Evandro Leopoldino Gonçalves (@evandrolg) <evandrolgoncalves@gmail.com>',
license = 'MIT <http://opensource.org/licenses/MIT>',
homepage = "https://github.com/" .. github_account_name .. "/" .. github_repo_name,
}

dependencies = {
"lua >= 5.1",
"mimetypes >= 1.0.0-1",
"luasocket >= 0.1.0-0",
"luafilesystem >= 1.6",
"lzlib >= 0.4.1.53-1",
}

build = {
type = "builtin",
modules = {
['pegasus.init'] = "src/pegasus/init.lua",
['pegasus.handler'] = 'src/pegasus/handler.lua',
['pegasus.request'] = 'src/pegasus/request.lua',
['pegasus.response'] = 'src/pegasus/response.lua',
['pegasus.compress'] = 'src/pegasus/compress.lua',
['pegasus.log'] = 'src/pegasus/log.lua',
['pegasus.plugins.compress'] = 'src/pegasus/plugins/compress.lua',
['pegasus.plugins.downloads'] = 'src/pegasus/plugins/downloads.lua',
['pegasus.plugins.files'] = 'src/pegasus/plugins/files.lua',
['pegasus.plugins.router'] = 'src/pegasus/plugins/router.lua',
['pegasus.plugins.tls'] = 'src/pegasus/plugins/tls.lua',
}
}


125 changes: 125 additions & 0 deletions src/pegasus/handler.lua
Original file line number Diff line number Diff line change
@@ -1,10 +1,70 @@
--- Module `pegasus.handler`
--
-- 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
-- 4. `pluginsBeforeProcess(request, response)` runs
-- 5. User `callback(request, response)` is invoked
-- 6. `pluginsAfterProcess(request, response)` runs
-- 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 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

--- 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`).
--
-- @tparam function callback user 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')
Expand All @@ -27,6 +87,8 @@ function Handler:new(callback, location, plugins, logger)
return result
end

--- 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
if plugin.alterRequestResponseMetaTable then
Expand All @@ -38,6 +100,12 @@ function Handler:pluginsAlterRequestResponseMetatable()
end
end

--- 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
Expand All @@ -50,6 +118,14 @@ function Handler:pluginsNewConnection(client)
return client
end

--- 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
Expand All @@ -61,6 +137,14 @@ function Handler:pluginsNewRequestResponse(request, response)
end
end

--- 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
Expand All @@ -72,6 +156,14 @@ function Handler:pluginsBeforeProcess(request, response)
end
end

--- 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
Expand All @@ -83,6 +175,16 @@ function Handler:pluginsAfterProcess(request, response)
end
end

--- 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
Expand All @@ -95,6 +197,17 @@ function Handler:pluginsProcessFile(request, response, filename)
end
end

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

Expand All @@ -112,6 +225,18 @@ function Handler:processBodyData(data, stayOpen, response)
return localData
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
function Handler:processRequest(port, client, server)
client = self:pluginsNewConnection(client)
if not client then
Expand Down
81 changes: 81 additions & 0 deletions src/pegasus/init.lua
Original file line number Diff line number Diff line change
@@ -1,12 +1,73 @@
--- Module `pegasus`
--
-- 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.
--
-- @module pegasus

local socket = require 'socket'
local Handler = require 'pegasus.handler'

-- require lualogging if available, "pegasus.log" will automatically pick it up
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
function Pegasus:new(params)
params = params or {}
local server = {}
Expand All @@ -21,6 +82,26 @@ function Pegasus:new(params)
return setmetatable(server, self)
end

--- Start the server accept loop (blocking).
--
-- The provided callback is invoked once per incoming HTTP request.
--
-- Example:
-- ```lua
-- 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)
-- ```
--
-- 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))
Expand Down
Loading
Loading