Plugin Manager Documentation
Table of Contents
Overview
The Plugin Manager is a Python-based system that enables dynamic Lua plugin loading for extending application functionality. It provides a secure, sandboxed environment for plugins to register HTTP routes, hook into application events, and manipulate HTML/Markdown content.
Key Features:
- Hot-reload plugins without restarting the application
- Secure file operations with path traversal protection
- Built-in HTML and Markdown manipulation tools
- Event-based hook system
- HTTP route registration
- Comprehensive logging
- Rate limiting and validation helpers
Architecture
Directory Structure
project/
├── plugin_manager.py # Main plugin manager
├── plugins/ # Lua plugin files (*.lua)
├── html/ # HTML files for manipulation
└── markdown/ # Markdown files for manipulation
Component Overview
PluginManager: Core class that manages plugin lifecycle, Lua runtime, and API exposure
PluginFSHandler: Watchdog-based file system handler for hot-reloading
LuaRuntime: Embedded Lua interpreter with Python bindings via lupa
Getting Started
Installation Requirements
pip install lupa watchdog
Creating Your First Plugin
- Create a file
plugins/hello.lua:
-- Register a simple route
add_route("/hello", function(req)
return "<h1>Hello from Lua Plugin!</h1>"
end)
log("Hello plugin loaded")
- Start the plugin manager:
from plugin_manager import PluginManager
manager = PluginManager()
manager.load_all()
# Handle a request
result = manager.handle_request("/hello")
print(result) # (200, {"Content-Type": "text/html"}, "<h1>Hello from Lua Plugin!</h1>")
- The plugin will automatically reload when you modify
hello.lua
Core Concepts
Plugins
Plugins are Lua scripts (.lua files) placed in the plugins/ directory. Each plugin can:
- Register HTTP routes via
add_route() - Hook into events via
register_hook() - Access file operations
- Use HTML and Markdown manipulation tools
Plugin Lifecycle:
- Load: Plugin file is read and executed in Lua runtime
- Active: Plugin routes/hooks are registered and callable
- Reload: File change detected → unload → load
- Unload: Plugin deleted → routes/hooks removed
Routes
Routes map URL paths to Lua handler functions. When a request matches a registered route, the corresponding Lua function is called.
See: Route Registration, Examples - Route Handler
Hooks
Hooks are named events where plugins can register callback functions. Multiple plugins can hook into the same event, and they'll be called in registration order.
Common Use Cases:
- Pre/post-processing request data
- Modifying responses before they're sent
- Content transformation pipelines
- Event notifications
See: Hook Registration, Examples - Hook System
API Reference
Route Registration
add_route(path, handler_function)
Register a URL path handler.
Parameters:
path(string): URL path to handle (e.g.,/api/users)handler_function(function): Lua function to handle requests
Handler Function Signature:
function(request) -> response
Request Object:
{
method = "GET", -- HTTP method
path = "/api/users", -- Request path
query = {...}, -- Query parameters
body = "...", -- Request body
headers = {...} -- Request headers
}
Response Format:
Option 1 - Simple string (returns 200 with text/html):
return "<html>...</html>"
Option 2 - Full response tuple:
return 200, {["Content-Type"] = "application/json"}, '{"status":"ok"}'
Example:
add_route("/api/status", function(req)
return 200, {["Content-Type"] = "application/json"}, '{"status": "online"}'
end)
Hook Registration
register_hook(hook_name, handler_function)
Register a callback for a named hook event.
Parameters:
hook_name(string): Name of the hook to register forhandler_function(function): Callback function
Handler Function Signature:
function(...args) -> result
Behavior:
- All registered hooks for an event are called in order
- Return values can modify data (last non-nil value is used)
- Hooks receive arguments passed by
run_hook()from Python
Example:
-- Content transformation hook
register_hook("transform_html", function(html)
-- Add analytics script
return html_insert_before(html, "</body>", "<script>analytics.js</script>")
end)
-- Logging hook
register_hook("request_complete", function(path, status)
log("Request to " .. path .. " returned " .. status)
end)
Python Side - Triggering Hooks:
# Transform HTML through all registered hooks
modified_html = manager.run_hook("transform_html", original_html)
# Notify all hooks of event
manager.run_hook("request_complete", "/api/users", 200)
See also: Routes, Examples - Hooks
File Operations
read_file(path)
Read any file with path validation.
Parameters:
path(string): File path to read
Returns: File content as string, or nil on error
Security: Path traversal (..) is blocked
Example:
local config = read_file("config.json")
if config then
log("Config loaded")
end
write_file(path, content)
Write content to file with path validation.
Parameters:
path(string): File path to writecontent(string): Content to write
Returns: true on success, false on error
Example:
write_file("output.txt", "Generated content")
See also: HTML Operations, Markdown Operations
HTML Manipulation
read_html(filename)
Read HTML file from the html/ directory.
Parameters:
filename(string): Name of HTML file (e.g.,"index.html")
Returns: HTML content as string, or nil if not found
Example:
local html = read_html("index.html")
if html then
-- Process HTML
end
write_html(filename, content)
Write HTML content to the html/ directory.
Parameters:
filename(string): Name of HTML filecontent(string): HTML content to write
Returns: true on success, false on error
Example:
local modified = html_insert_after(html, "<head>", "<meta name='theme' content='dark'>")
write_html("index.html", modified)
list_html_files()
Get list of all HTML files in the html/ directory.
Returns: Array of filenames
Example:
local files = list_html_files()
for _, file in ipairs(files) do
log("Found HTML file: " .. file)
end
html_find_tag(html, tag)
Find the first occurrence of an HTML tag.
Parameters:
html(string): HTML content to searchtag(string): Tag name (e.g.,"div","title")
Returns: Matched tag with content, or nil if not found
Example:
local title_tag = html_find_tag(html, "title")
-- Result: "<title>My Page</title>"
html_replace_tag(html, tag, new_content)
Replace the content inside an HTML tag.
Parameters:
html(string): HTML contenttag(string): Tag name to replace content withinnew_content(string): New content (tag itself is preserved)
Returns: Modified HTML
Example:
-- Replace title content
local html = "<title>Old Title</title>"
local modified = html_replace_tag(html, "title", "New Title")
-- Result: "<title>New Title</title>"
See also: html_wrap_content()
html_insert_before(html, marker, content)
Insert content before a marker string.
Parameters:
html(string): HTML contentmarker(string): String to insert beforecontent(string): Content to insert
Returns: Modified HTML
Example:
-- Insert meta tag before closing head
local html = "<head></head><body></body>"
local modified = html_insert_before(html, "</head>", "<meta charset='utf-8'>")
-- Result: "<head><meta charset='utf-8'></head><body></body>"
See also: html_insert_after()
html_insert_after(html, marker, content)
Insert content after a marker string.
Parameters:
html(string): HTML contentmarker(string): String to insert aftercontent(string): Content to insert
Returns: Modified HTML
Example:
-- Insert script before closing body
local modified = html_insert_after(html, "<body>", "<script>init();</script>")
See also: html_insert_before()
html_wrap_content(html, tag, wrapper_tag, attrs)
Wrap the content of a tag with another tag.
Parameters:
html(string): HTML contenttag(string): Tag whose content to wrapwrapper_tag(string): Tag to wrap withattrs(string): Attributes for wrapper tag (optional)
Returns: Modified HTML
Example:
local html = "<div>Hello World</div>"
local modified = html_wrap_content(html, "div", "span", 'class="highlight"')
-- Result: "<div><span class="highlight">Hello World</span></div>"
See also: html_replace_tag()
Markdown Manipulation
read_markdown(filename)
Read Markdown file from the markdown/ directory.
Parameters:
filename(string): Name of Markdown file (e.g.,"README.md")
Returns: Markdown content as string, or nil if not found
Example:
local md = read_markdown("README.md")
write_markdown(filename, content)
Write Markdown content to the markdown/ directory.
Parameters:
filename(string): Name of Markdown filecontent(string): Markdown content to write
Returns: true on success, false on error
Example:
write_markdown("output.md", "# Generated Document\n\nContent here...")
list_markdown_files()
Get list of all Markdown files in the markdown/ directory.
Returns: Array of filenames
Example:
local files = list_markdown_files()
for _, file in ipairs(files) do
local content = read_markdown(file)
-- Process each file
end
md_add_header(markdown, level, text)
Add a header at the beginning of Markdown content.
Parameters:
markdown(string): Existing Markdown contentlevel(number): Header level (1-6, where 1 is#and 6 is######)text(string): Header text
Returns: Modified Markdown with header prepended
Example:
local md = "Some content here"
local modified = md_add_header(md, 1, "Introduction")
-- Result:
-- # Introduction
--
-- Some content here
See also: md_replace_section()
md_replace_section(markdown, header, new_content)
Replace an entire Markdown section (from header to next header or end).
Parameters:
markdown(string): Markdown contentheader(string): Header text to find (without#symbols)new_content(string): New content for the section
Returns: Modified Markdown
Example:
local md = [[
# Introduction
Old intro text
# Features
Feature list here
]]
local modified = md_replace_section(md, "Introduction", "New introduction paragraph")
-- Result:
-- ## Introduction
--
-- New introduction paragraph
--
-- # Features
-- Feature list here
See also: md_add_header(), md_append_content()
md_append_content(markdown, content)
Append content to the end of Markdown document.
Parameters:
markdown(string): Existing Markdown contentcontent(string): Content to append
Returns: Modified Markdown
Example:
local md = "# Document\n\nExisting content"
local modified = md_append_content(md, "## Appendix\n\nAdditional information")
See also: md_add_header()
Logging Functions
log(message)
Log informational message.
Parameters:
message(string): Message to log
Example:
log("Plugin initialized successfully")
See also: log_warn(), log_error()
log_warn(message)
Log warning message.
Parameters:
message(string): Warning message
Example:
if not config then
log_warn("Configuration file not found, using defaults")
end
See also: log(), log_error()
log_error(message)
Log error message.
Parameters:
message(string): Error message
Example:
local html = read_html("template.html")
if not html then
log_error("Failed to load template.html")
return
end
See also: log(), log_warn()
Utility Functions
table_to_json(lua_table)
Convert Lua table to JSON string.
Parameters:
lua_table(table): Lua table to convert
Returns: JSON string
Example:
local data = {name = "John", age = 30}
local json = table_to_json(data)
-- Result: '{"name":"John","age":30}'
See also: json_to_table()
json_to_table(json_str)
Parse JSON string into Lua table.
Parameters:
json_str(string): JSON string
Returns: Lua table, or empty table on error
Example:
local json = '{"status":"ok","count":5}'
local data = json_to_table(json)
log("Status: " .. data.status) -- Status: ok
See also: table_to_json()
Guardrails & Security
The plugin system includes built-in security features and helper functions to prevent common vulnerabilities.
Path Traversal Protection
All file operations automatically block path traversal attempts:
-- BLOCKED - Will fail safely
read_file("../../../etc/passwd")
read_html("../../config.php")
-- ALLOWED - Sandboxed to designated directories
read_html("index.html")
read_markdown("docs.md")
Implementation: Any path containing .. is rejected before file access.
Safe String Operations
safe_concat(...)
Safely concatenate multiple values, converting to strings and handling nil.
Example:
local msg = safe_concat("User ", nil, " logged in at ", os.time())
-- Result: "User logged in at 1609459200"
See also: escape_html()
escape_html(str)
Escape HTML special characters to prevent XSS attacks.
Escapes:
&→&<→<>→>"→"'→'
Example:
local user_input = '<script>alert("XSS")</script>'
local safe = escape_html(user_input)
-- Result: "<script>alert("XSS")</script>"
return "<div>" .. safe .. "</div>"
Use Case: Always escape user input before including in HTML responses.
See also: Security Best Practices
Filename Validation
is_valid_filename(name)
Validate filename for safety.
Checks:
- Not nil or empty
- No
..(path traversal) - No
/or\(directory separators)
Returns: true if valid, false otherwise
Example:
local filename = req.query.file
if not is_valid_filename(filename) then
return 400, {}, "Invalid filename"
end
local content = read_html(filename)
See also: Path Traversal Protection
Error Handling
try_catch(fn, catch_fn)
Safe error handling wrapper using pcall.
Parameters:
fn(function): Function to executecatch_fn(function): Error handler (receives error message)
Returns: true if successful, false if error occurred
Example:
local success = try_catch(
function()
local html = read_html("template.html")
local modified = html_replace_tag(html, "title", "New Title")
write_html("output.html", modified)
end,
function(err)
log_error("HTML processing failed: " .. tostring(err))
end
)
if not success then
return 500, {}, "Internal error"
end
See also: validate_request()
Request Validation
validate_request(req, required_fields)
Validate request object has required fields.
Parameters:
req(table): Request object to validaterequired_fields(array): List of required field names
Returns: (true, nil) if valid, or (false, error_message) if invalid
Example:
add_route("/api/create", function(req)
local valid, err = validate_request(req, {"name", "email", "body"})
if not valid then
return 400, {}, "Bad request: " .. err
end
-- Process valid request
return 200, {}, "Created"
end)
See also: try_catch()
Rate Limiting
check_rate_limit(key, max_calls, window_seconds)
Simple in-memory rate limiting.
Parameters:
key(string): Unique identifier for rate limit (e.g., IP address, user ID)max_calls(number): Maximum calls allowed in windowwindow_seconds(number): Time window in seconds
Returns: true if within limit, false if exceeded
Example:
add_route("/api/search", function(req)
local client_ip = req.headers["X-Forwarded-For"] or "unknown"
if not check_rate_limit(client_ip, 10, 60) then
return 429, {}, "Rate limit exceeded. Try again later."
end
-- Process search
return 200, {}, "Search results"
end)
Note: Rate limits are stored in memory and reset when plugin reloads.
See also: Security Best Practices
Table Utilities
table_contains(tbl, value)
Check if table contains a value.
Example:
local allowed = {"admin", "moderator", "user"}
if table_contains(allowed, req.role) then
-- Process request
end
table_keys(tbl)
Get all keys from a table.
Example:
local data = {name = "John", age = 30}
local keys = table_keys(data)
-- Result: {"name", "age"}
table_values(tbl)
Get all values from a table.
Example:
local data = {a = 1, b = 2, c = 3}
local values = table_values(data)
-- Result: {1, 2, 3}
Examples
Example 1: Simple Route Handler
Create plugins/api.lua:
-- Simple API route
add_route("/api/hello", function(req)
local name = req.query.name or "World"
local safe_name = escape_html(name)
return 200,
{["Content-Type"] = "application/json"},
'{"message": "Hello ' .. safe_name .. '"}'
end)
log("API plugin loaded")
Usage:
result = manager.handle_request("/api/hello", {
"query": {"name": "Alice"}
})
# Returns: (200, {...}, '{"message": "Hello Alice"}')
See also: Route Registration
Example 2: Hook System
Create plugins/analytics.lua:
-- Hook into HTML transformation
register_hook("transform_html", function(html, page_name)
log("Adding analytics to " .. page_name)
local analytics_script = [[
<script>
window.analytics = {
page: "]] .. page_name .. [[",
timestamp: ]] .. os.time() .. [[
};
</script>
]]
return html_insert_before(html, "</head>", analytics_script)
end)
Python Side:
# Trigger hook
original_html = "<html><head></head><body>...</body></html>"
modified_html = manager.run_hook("transform_html", original_html, "home")
See also: Hook Registration
Example 3: HTML Template Processor
Create plugins/templates.lua:
-- Process HTML templates with variable substitution
add_route("/render/:template", function(req)
local template_name = req.params.template .. ".html"
-- Validate filename
if not is_valid_filename(template_name) then
return 400, {}, "Invalid template name"
end
-- Read template
local html = read_html(template_name)
if not html then
return 404, {}, "Template not found"
end
-- Replace variables
local title = req.query.title or "Untitled"
local content = req.query.content or "No content"
html = html_replace_tag(html, "title", escape_html(title))
html = html_replace_tag(html, "main", escape_html(content))
return html
end)
See also: HTML Manipulation, Route Registration
Example 4: Markdown Documentation Generator
Create plugins/docs_generator.lua:
-- Generate documentation from code files
add_route("/docs/generate", function(req)
local doc_content = [[
# API Documentation
Generated on: ]] .. os.date() .. [[
## Overview
This documentation is auto-generated from source code.
]]
-- Add sections for each endpoint
local endpoints = {
{path = "/api/users", method = "GET", desc = "Get all users"},
{path = "/api/users/:id", method = "GET", desc = "Get user by ID"},
{path = "/api/users", method = "POST", desc = "Create new user"}
}
for _, endpoint in ipairs(endpoints) do
local section = string.format([[
## %s %s
%s
**Example:**
curl -X %s http://localhost%s
]], endpoint.method, endpoint.path, endpoint.desc, endpoint.method, endpoint.path)
doc_content = md_append_content(doc_content, section)
end
-- Save to file
write_markdown("api_docs.md", doc_content)
return 200, {}, "Documentation generated successfully"
end)
See also: Markdown Manipulation
Example 5: Content Security System
Create plugins/security.lua:
-- Rate-limited API with validation
add_route("/api/submit", function(req)
-- Rate limiting
local client_ip = req.headers["X-Real-IP"] or "unknown"
if not check_rate_limit(client_ip, 5, 60) then
log_warn("Rate limit exceeded for " .. client_ip)
return 429, {}, "Too many requests"
end
-- Request validation
local valid, err = validate_request(req, {"title", "content", "author"})
if not valid then
return 400, {}, err
end
-- Content sanitization
local title = escape_html(req.body.title)
local content = escape_html(req.body.content)
local author = escape_html(req.body.author)
-- Save as HTML
local html = string.format([[
<!DOCTYPE html>
<html>
<head>
<title>%s</title>
</head>
<body>
<h1>%s</h1>
<p>By: %s</p>
<div>%s</div>
</body>
</html>
]], title, title, author, content)
local filename = "post_" .. os.time() .. ".html"
write_html(filename, html)
log("Created post: " .. filename)
return 200, {}, "Post created successfully"
end)
See also: Guardrails & Security
Example 6: Multi-File HTML Processor
Create plugins/html_batch.lua:
-- Process all HTML files in a directory
add_route("/admin/add_footer", function(req)
local files = list_html_files()
local processed = 0
local footer = [[
<footer style="text-align:center; padding:20px; background:#f0f0f0;">
<p>© 2025 My Website. All rights reserved.</p>
</footer>
]]
for _, filename in ipairs(files) do
try_catch(
function()
local html = read_html(filename)
if html then
-- Add footer before closing body tag
local modified = html_insert_before(html, "</body>", footer)
write_html(filename, modified)
processed = processed + 1
end
end,
function(err)
log_error("Failed to process " .. filename .. ": " .. tostring(err))
end
)
end
return 200, {}, "Processed " .. processed .. " files"
end)
See also: HTML Manipulation, Error Handling
Best Practices
Plugin Design
- Single Responsibility: Each plugin should have one clear purpose
- Error Handling: Always use
try_catch()for operations that might fail - Logging: Use appropriate log levels (
log(),log_warn(),log_error()) - Documentation: Add comments explaining what your plugin does
-- Good: Clear purpose, error handling, logging
add_route("/api/config", function(req)
try_catch(
function()
local config = read_file("config.json")
return 200, {}, config
end,
function(err)
log_error("Config read failed: " .. tostring(err))
return 500, {}, "Internal error"
end
)
end)
See also: Examples
Security
- Always Escape User Input: Use
escape_html()for any user-provided content - Validate Filenames: Use
is_valid_filename()before file operations - Validate Requests: Use
validate_request()for required fields - Implement Rate Limiting: Use
check_rate_limit()for public APIs - Never Trust Input: Sanitize and validate all external data
-- Good: Comprehensive security
add_route("/api/read", function(req)
local filename = req.query.file
-- Validate filename
if not is_valid_filename(filename) then
return 400, {}, "Invalid filename"
end
-- Rate limit
if not check_rate_limit(req.ip, 10, 60) then
return 429, {}, "Rate limited"
end
-- Safe read
local content = read_html(filename)
if content then
return escape_html(content)
end
return 404, {}, "Not found"
end)
See also: Guardrails & Security, Path Traversal Protection