#!/usr/bin/env lua5.3
local env = os.getenv
local f = io.open("/dev/urandom", 'r')
local e = f and f:read(1)
if f then f:close() end
math.randomseed(os.time() + string.byte(e))
local function url_encode(str)
return (str:gsub("([^A-Za-z0-9%_%.%-%~])", function(v)
return string.upper(string.format("%%%02x", string.byte(v)))
end))
end
local esc_sequences = {
["<"] = "<",
[">"] = ">",
['"'] = """
}
local function html_encode(x)
local escaped = tostring(x)
escaped = escaped:gsub("&", "&")
for char,esc in pairs(esc_sequences) do
escaped = string.gsub(escaped, char, esc)
end
return escaped
end
local function parse_qs(str,sep)
sep = sep or '&'
local function decode(str, path)
local str = str
if not path then
str = str:gsub('+', ' ')
end
str = str:gsub("%%(%x%x)", function(c)
return string.char(tonumber(c, 16))
end)
return (str:gsub('\r\n', '\n'))
end
local values = {}
for key,val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) do
local key = decode(key)
local keys = {}
key = key:gsub('%[([^%]]*)%]', function(v)
-- extract keys between balanced brackets
if string.find(v, "^-?%d+$") then
v = tonumber(v)
else
v = decode(v)
end
table.insert(keys, v)
return "="
end)
key = key:gsub('=+.*$', "")
key = key:gsub('%s', "_") -- remove spaces in parameter name
val = val:gsub('^=+', "")
if not values[key] then
values[key] = {}
end
if #keys > 0 and type(values[key]) ~= 'table' then
values[key] = {}
elseif #keys == 0 and type(values[key]) == 'table' then
values[key] = decode(val)
end
local t = values[key]
for i,k in ipairs(keys) do
if type(t) ~= 'table' then
t = {}
end
if k == "" then
k = #t+1
end
if not t[k] then
t[k] = {}
end
if i == #keys then
t[k] = decode(val)
end
t = t[k]
end
end
return values
end
local cookies = env "HTTP_COOKIE" and parse_qs(env "HTTP_COOKIE","; ") or {}
local history = {}
if cookies.history then
for page in cookies.history:gmatch "(%w%w%w%w%w)%," do
table.insert(history, page)
end
end
local function redirect(to)
return "", {
status = '303 see other',
headers = { location = to },
}
end
local function template(str)
return function (t)
return (str:gsub("$([A-Za-z][A-Za-z0-9]*)", function(v)
return t[v] or ""
end))
end
end
local base = template [[
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="/static/amethyst.css" />
<meta charset="utf-8"/>
<title>zzcxz: $title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>zzcxz</h1>
<main>$content</main>
<footer>
<div class="links">
<p><a href="/">zzcxz</a></p>
<p><a href="/about">help</a></p>
</div>
</footer>
</body>
</html>
]]
local not_found = function()
return
base {
title = "not found",
content = "the content requested was not found.",
}, { status = '404 not found' }
end
local function parse_directive(line, directives)
local directive, args = line:match "^#([A-Za-z]+)%s*(.-)\n?$"
directive = directive and directive:lower()
if not directive then
return
elseif directive == "redirect" then
local redirect = args:match "^%s*(%w%w%w%w%w)%s*$"
if not redirect then return end
directives.redirect = redirect
else
return
end
return true
end
local load_page
local function convert_markup(m)
local result = {}
local directives = {}
local code_block = false
for line in (m..'\n'):gmatch "(.-)\n" do
if not code_block then
if line:match "^%s*$" then
goto continue
end
if line:sub(1,1) == '#' and
parse_directive(line, directives) then
if directives.redirect then
local to = load_page(directives.redirect)
if to then
local m, d = convert_markup(to.content)
-- the final destination will not have a redirect
-- value in this directive. as such, this will store
-- final destination of a chain of redirects.
d.redirect = d.redirect or to
return m, d
else
directives.redirect = nil
end
else
goto continue
end
end
line = html_encode(line)
if line:sub(1,1) == ' ' then
table.insert(result, '<pre><code>')
code_block = true
else
line = line:gsub("\\\\([%[%]\\])", "\%1")
line = line:gsub("\\([%[%]])",
{ ['['] = "[", [']'] = "]" })
line = line:gsub("%[(.-)%]",
function(s)
return ('<span class="important">%s</span>'):format(s)
end
)
table.insert(result, ('<p>%s</p>'):format(line))
end
end
if code_block then
if line:sub(1,1) == ' ' then
table.insert(result, line .. '\n')
else
table.insert(result, '</code></pre>')
code_block = false
end
end
::continue::
end
if code_block then
table.insert(result, '</code></pre>')
code_block = false
end
return table.concat(result), directives
end
local function parse_page(s)
local page = {}
page.title = s:match "^(.-)\n"
page.actions = {}
local content = {}
for line in (s..'\n'):gmatch "(.-\n)" do
if line:sub(1,1) == '\t' then
table.insert(content, line:sub(2))
elseif line:match("^!image") then
page.illustration = line:match "^!image%s+(%w+)"
else
local target, action = line:match "^(%w%w%w%w%w):(.-)\n$"
if action then
table.insert(page.actions, {action = action, target = target})
end
end
end
page.content = table.concat(content)
return page
end
function load_page(p, raw)
if not p:match("^%w%w%w%w%w$") then return nil end
local f, bee = io.open('content/'..p)
if not f then return nil end
local s = f:read "a"
f:close()
if not s then return nil end
if raw then return s end
local page = parse_page(s)
page.id = p
return page
end
local function new_action(page, action, result)
local _, directives = convert_markup(result)
::generate_name::
local new_name = {}
for i=1,5 do
table.insert(new_name, string.char(string.byte 'a' + math.random(0,25)))
end
new_name = table.concat(new_name)
local exists = io.open('content/'..new_name, 'r')
if exists then
exists:close()
goto generate_name
end
local old = assert(io.open('content/'..page, 'a'))
local new = assert(io.open('content/'..new_name, 'w'))
action = action:gsub('\n', ' ')
assert(new:write(action..'\n'))
for line in (result..'\n'):gmatch "(.-\n)" do
assert(new:write('\t' .. line))
end
assert(old:write(('%s:%s\n'):format(new_name, action)))
if env "REMOTE_ADDR" then
new:write(('!ip %s\n'):format(env "REMOTE_ADDR"))
end
if directives.backlinks then
for _,d in ipairs(directives.backlinks) do
assert(new:write(('%s:%s\n'):format(d.page, d.action)))
end
end
new:close()
old:close()
return new_name
end
local hist_template = template [[
<ul class="hist-log">
$log
</ul>
]]
local function show_hist(show_ids)
local log = {}
if #history == 0 then return "" end
for i=#history,1,-1 do
local page = load_page(history[i])
if not page then goto continue end
-- highlight the current page
local title = i ~= #history and html_encode(page.title)
or '<strong>'..html_encode(page.title)..'</strong>'
if show_ids then
table.insert(log,
('<li>%s <span class="page-id">%s</span></li>')
:format(title, history[i]))
else
table.insert(log, ('<li>%s</li>'):format(title))
end
::continue::
end
return hist_template {
log = table.concat(log),
}
end
local map = {}
local page_template = template [[
<h2>$title</h2>
$illustration
$content
$drawthis
<ul class="actions">
$actions
</ul>
$log
]]
local draw_this = [[
<p id="draw-this"><a href="$page/illustrate">illustrate this</a></p>
]]
map["^/g/(%w%w%w%w%w)$"] = function(p)
local page = load_page(p)
if not page then return not_found() end
local content, directives = convert_markup(page.content)
if env "REQUEST_METHOD" ~= "POST" then
if history[#history] ~= p then
table.insert(history, p)
end
if #history > 75 then
table.remove(history, 1)
end
local title = page.title
if directives.redirect then
page = directives.redirect
end
local actions = {}
for _,a in ipairs(page.actions) do
table.insert(actions,
('<li><a href="%s">%s</a></li>'):format(
html_encode(a.target), html_encode(a.action)))
end
if not directives.deadend then
table.insert(actions,
([[
<li>
<a class="important" href="%s/act?back=%s#what">%s</a>
</li>
]]):format(page.id, p, #page.actions == 0 and
"do something..." or "do something else...")
)
end
local illustration, draw_this
if page.illustration then
illustration = ([[
<img class="illustration" src="/i/%s.%s" />
]]):format(page.id, page.illustration)
else
-- draw_this = ([[
-- <p id="draw-this"><a href="%s/illustrate">
-- illustrate this
-- </a></p>
-- ]]):format(p)
end
local hist_cookie = ('history=%s; path=/; secure; max-age=99999999999')
:format(table.concat(history, ',')..',')
return base {
title = html_encode(title),
content = page_template {
title = html_encode(title),
content = content,
actions = table.concat(actions),
illustration = illustration,
drawthis = draw_this,
log = show_hist(),
},
}, { headers = { ['set-cookie'] = hist_cookie } }
else
if directives.deadend then
return base {
title = "error",
content = "forbidden",
}, { status = '403 forbidden' }
end
local form = parse_qs(io.read "a")
form.wyd = form.wyd or "something"
form.happens = form.happens or "something"
if utf8.len(form.wyd) > 150 then form.wyd = "something" end
if utf8.len(form.happens) > 10000 then form.wyd = "something" end
local new = new_action(p, form.wyd, form.happens)
return redirect("/g/"..new)
end
end
map["^/g/(%w%w%w%w%w)/$"] = function(p)
return redirect('/g/'..p)
end
local edit_template = template [[
$content
<hr id="what"/>
$preview
<form method="POST">
<p>
<a href="/about#rules">READ THIS</a> before touching anything.
</p>
<h2>what do you do?</h2>
<input
type="text"
id="wyd" name="wyd"
value="$title"
maxlength="150" required
/>
<h2>what happens next?</h2>
<textarea
id="happens" name="happens"
maxlength="10000" required
>$editing</textarea>
<div class="buttons">
<a href="$cancel">cancel</a>
<input type="submit" value="preview" />
$submit
</div>
$log
</form>
]]
local preview_template = template [[
<h2>$title</h2>
$content
<hr />
]]
local submit_template = template [[
<input type="submit" formaction="/g/$page" value="submit" />
]]
map["^/g/(%w%w%w%w%w)/act$"] = function(p)
local page = load_page(p)
if not page then return not_found() end
local _, directives = convert_markup(page.content)
if directives.deadend then return not_found() end
local query = parse_qs(env "QUERY_STRING" or "")
local cancel = '/g/'..(query.back or p)
if env "REQUEST_METHOD" ~= "POST" then
return base {
title = "do something new",
content = edit_template {
page = p,
content = convert_markup(page.content),
log = show_hist(true),
cancel = html_encode(cancel),
},
}
else
local form = parse_qs(io.read "a")
form.wyd = form.wyd or "something"
form.happens = form.happens or "something"
local prev, prev_direct = convert_markup(form.happens)
local prev_title =
prev_direct.title and html_encode(prev_direct.title) or
html_encode(form.wyd)
if prev_direct.redirect then
local note =
('<span class="note">previewing %s</span>')
:format(prev_direct.redirect.id)
prev = note..prev
end
return base {
title = "do something new",
content = edit_template {
page = p,
content = convert_markup(page.content),
preview = preview_template {
title = prev_title,
content = prev,
},
title = html_encode(form.wyd),
editing = html_encode(form.happens),
submit = submit_template { page = p },
cancel = html_encode(cancel),
log = show_hist(true),
},
}
end
end
local illustrate_template = template [[
<h2>$title</h2>
$content
<hr/>
<h2 id="what">what does this look like?</h2>
<form method="POST" action="/g/$page/illustrate"
enctype="multipart/form-data" id="img-form">
<input
type="file"
name="file"
accept="image/png,image/jpeg,image/gif"
/>
<input type="submit" value="submit image" />
</form>
]]
-- map["^/g/(%w%w%w%w%w)/illustrate"] = function(p)
-- local page = load_page(p)
-- if not page then return not_found() end
--
-- if env "REQUEST_METHOD" ~= "POST" then
-- return base {
-- title = "illustration: " .. html_encode(page.title),
-- content = illustrate_template {
-- title = html_encode(page.title),
-- content = convert_markup(page.content),
-- page = p,
-- },
-- }
-- else
-- end
-- end
map["^/i/(%w%w%w%w%w).(%w+)$"] = function(p, format)
local page = load_page(p)
if not page or
not page.illustration or not page.illustration == format then
return not_found()
end
return
assert(io.open(('content/%s.%s'):format(p, format), 'r')),
{ content_type = 'image/'..format }
end
map["^/g/(%w%w%w%w%w)/raw$"] = function(p)
local page = load_page(p, true)
if not page then return not_found() end
page = page:gsub("\n!ip [^\n]*", "")
return page, {
content_type = 'text/plain',
headers = { ['access-control-allow-origin'] = "*" }
}
end
map["^/g/?$"] = function()
if #history > 0 then
return redirect('/g/'..history[#history])
else
return redirect '/g/zzcxz'
end
end
map["^/about/?$"] = function()
return assert(io.open("about.html", 'r'))
end
map["^/robots.txt$"] = function()
return assert(io.open("robots.txt", 'r')),
{ content_type = 'text/plain' }
end
local index_template = template [[
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="/static/amethyst.css" />
<meta charset="utf-8" />
<title>zzcxz</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:title" content="zzcxz" />
<meta property="og:description" content="the zzcxz is to be entered. do not believe what you read" />
</head>
<body>
<main class="index">
<img class="logo" src="/static/zzcxz.png" alt="zzcxz" />
<ul class="actions">
$actions
<li><a href="/about">help</a></li>
</ul>
</main>
</body>
</html>
]]
map["^/$"] = function()
local last_page =
load_page(history[#history] or 'zzcxz') or load_page 'zzcxz'
local actions = ('<li><a href="/g/%s" class="important">%s</a></li>')
:format(last_page.id, html_encode(last_page.title))
if last_page.id ~= 'zzcxz' then
actions = actions .. '<li><a href="/g/zzcxz">reenter the zzcxz</a></li>'
end
return index_template {actions = actions}
end
local function main()
for k,v in pairs(map) do
local m = {(env "PATH_INFO"):match(k)}
if m[1] then
return v(table.unpack(m))
end
end
return not_found()
end
local ok, content, resp = pcall(main)
if not ok or (type(content) ~= 'string' and type(content) ~= 'userdata') then
io.stderr:write(content..'\n')
content = base {
title = "internal error",
content = "an internal error occurred."
}
resp = { status = '500 internal server error' }
end
resp = resp or {}
resp.content_type = resp.content_type or 'text/html'
resp.status = resp.status or '200 OK'
resp.headers = resp.headers or {}
resp.headers['content-type'] = resp.content_type
print("status: "..resp.status)
for k,v in pairs(resp.headers) do
print(("%s: %s"):format(k, v))
end
print ""
if type(content) == 'string' then
io.write(content)
else
while 1 do
local data = content:read(1024)
if not data then break end
io.write(data)
end
end