-- ***********************************************************************
--
-- Copyright 2016 by Sean Conner.
--
-- This program is free software: you can redistribute it and/or modify it
-- under the terms of the GNU General Public License as published by the
-- Free Software Foundation, either version 3 of the License, or (at your
-- option) any later version.
--
-- This program is distributed in the hope that it will be useful, but
-- WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
-- Public License for more details.
--
-- You should have received a copy of the GNU General Public License along
-- with this program. If not, see <
http://www.gnu.org/licenses/>.
--
-- Comments, questions and criticisms can be sent to:
[email protected]
--
-- =======================================================================
--
-- Code to handle gopher requests.
--
-- ***********************************************************************
-- luacheck: globals INFO FILE DIR ERROR HTML ACL
-- luacheck: globals config blog path_okay init main
-- luacheck: globals display_page display_request display_text_file
-- luacheck: globals display_fs_object display_index_file
-- luacheck: ignore 611
local process = require "org.conman.process"
local syslog = require "org.conman.syslog"
local fsys = require "org.conman.fsys"
local char = require "org.conman.parsers.ascii.char"
+ require "org.conman.parsers.utf8.char"
local lpeg = require "lpeg"
local bible = require "bible"
local movie = require "movie"
local get = require "get"
local string = require "string"
local io = require "io"
local table = require "table"
local config = config
local blog = blog
local tostring = tostring
local ipairs = ipairs
local pairs = pairs
local type = type
local pcall = pcall
local _VERSION = _VERSION
if _VERSION == "Lua 5.1" then
module("handler")
else
_ENV = {}
end
-- ***********************************************************************
-- Access Control List---a list of patterns to be applied to files to
-- prevent them from being accessed.
-- ***********************************************************************
ACL =
{
{ "^%..*" , false } ,
{ ".*%~$" , false } ,
{ "/%.%./" , false } ,
{ "/%." , false } ,
{ "^build$" , false } ,
{ "^main$" , false } ,
{ "%.so$" , false } ,
{ "%.o$" , false } ,
{ "%.a$" , false } ,
{ ".*" , true }
}
-- ***********************************************************************
-- Usage: okay = handler.path_okay(acl,path)
-- Desc: Check a filename against an ACL list
-- Input: acl (table, see above)
-- path (string) filepath
-- Return: okay (boolean)
-- ***********************************************************************
function path_okay(acl,path)
for i = 1 , #acl do
if path:match(acl[i][1]) then
return acl[i][2]
end
end
return false
end
-- ***********************************************************************
-- Usage: link = handler.INFO(info)
-- Desc: Return a gopher formatted INFO link (text)
-- Input: info (table)
-- * [1] = INFO
-- * [2] = string, function or table
-- | if function, it should return text
-- | if table, an array of strings
-- | if string, use that
-- Return: link (string) a formatted INFO link
-- ***********************************************************************
function INFO(info)
if type(info[2]) == 'function' then
return INFO { INFO , info[2]() }
elseif type(info[2]) == 'table' then
local ret = ""
for i = 1 , #info[2] do
ret = ret .. INFO { INFO , info[2][i] }
end
return ret
else
return string.format("i%s\t\texample.org\t70\r\n",tostring(info[2]))
end
end
-- ***********************************************************************
-- Usage: link = handler.FILE(info)
-- Desc: Return a gopher formatted FILE link
-- Input: info (table)
-- * [1] = FILE
-- * [2] = label (string or function)
-- * [3] = selector (string or function)
-- * [4] = remotehost (string/optional)
-- * [5] = remoteport (string/optional)
-- Return: link (string) a formatted FILE link
-- ***********************************************************************
function FILE(info)
local host = info[4] or config.interface.hostname
local port = info[5] or config.interface.port
if type(info[2]) == 'function' then
info[2] = info[2]()
end
if type(info[3]) == 'function' then
info[3] = info[3]()
end
return string.format("0%s\t%s\t%s\t%s\r\n",info[2],info[3],host,port)
end
-- ***********************************************************************
-- Usage: link = handler.DIR(info)
-- Desc: Return a gopher formatted DIR link
-- Input: info (table)
-- * [1] = DIR
-- * [2] = label (string)
-- * [3] = selector (string)
-- * [4] = remotehost (string/optional)
-- * [5] = remoteport (string/optional)
-- Return: link (string) a formatted DIR link
-- ***********************************************************************
function DIR(info)
local host = info[4] or config.interface.hostname
local port = info[5] or config.interface.port
return string.format("1%s\t%s\t%s\t%s\r\n",info[2],info[3],host,port)
end
-- ***********************************************************************
-- Usage: link = handler.ERROR(info)
-- Desc: Return a gopher formatted ERROR
-- Input: info (table)
-- * [1] = ERROR
-- * [2] = label (string)
-- * [3] = selector (string)
-- * [4] = remotehost (string/optional)
-- * [5] = remoreport (string/optional)
-- Return: link (string) a formatted ERROR
-- ***********************************************************************
function ERROR(info)
local host = info[4] or config.interface.hostname
local port = info[5] or config.interface.port
return string.format("3%s\t%s\t%s\t%s\r\n",info[2],info[3],host,port)
end
-- ***********************************************************************
-- Usage: link = handler.HTML(info)
-- Desc: Return a gopher formatted HTML link
-- Input: info (table)
-- * [1] = HTML
-- * [2] = label (string)
-- * [3] = selector (string)
-- * [4] = remotehost (string/optional)
-- * [5] = remoteport (string/optional)
-- Return: link (string) a formatted HTML link
-- ***********************************************************************
function HTML(info)
local host = info[4] or config.interface.hostname
local port = info[5] or config.interface.port
return string.format("h%s\t%s\t%s\t%s\r\n",info[2],info[3],host,port)
end
-- ***********************************************************************
-- This array of formats is passed to the blog module.
-- ***********************************************************************
local FORMATS =
{
['INFO'] = INFO,
['FILE'] = FILE,
['DIR'] = DIR,
['ERROR'] = ERROR,
['HTML'] = HTML,
['info'] = INFO,
['file'] = FILE,
['dir'] = DIR,
['error'] = ERROR,
['html'] = HTML
}
-- ***********************************************************************
-- The main page, an array of gopher links. This should show more about how
-- the functions above are called.
-- ***********************************************************************
local top_page =
{
{ INFO , "Welcome to Conman Laboratories" },
{ INFO , "" },
{ INFO , "NOTE: RFC-1436 says this about selectors:" },
{ INFO , "" },
{ INFO , " ... an OPAQUE selector string ... The selector string should MEAN" },
{ INFO , " NOTHING to the client software; it should never be modified by the" },
{ INFO , " client." },
{ INFO , "" },
{ INFO , "(emphasis added)" },
{ INFO , "" },
{ INFO , "The selectors on this server *ARE OPAQUE* and *MUST* be sent *AS IS* to" },
{ INFO , "the server. Please note that the selectors here rarely start with a '/'" },
{ INFO , "character. Particularely, phlog entries start with a selector of" },
{ INFO , [["Phlog:"---note the lack of '/' and the ending ':'.]] },
{ INFO , "" },
{ INFO , "Thank you." },
{ INFO , " -- The Management" },
{ INFO , "" },
{ FILE , "About the server" , "About:Server" } ,
{ FILE , "About the author" , "About:Me" } ,
{ DIR , "Gopher Server Source Code" , "Gopher:Src:" } ,
{ DIR , "Boston source code" , "Boston:Src:" } ,
{ DIR , "CGILib source code" , "CGI:Src:" } ,
{ DIR , "The Boston Diaries Phlog Feed" , "phlog.gopher" } ,
{ DIR , "The Boston Diaries Phlog Archives" , "Phlog:" } ,
{ FILE , "Latest Phlog Post" , blog.last_link } ,
{ DIR , "The Electric King James Bible" , "Bible:" } ,
{ DIR , "The Quick and Dirty B-Movie Plot Generator" , "Movie:" } ,
{ INFO , "" } ,
{ DIR , "Other Gopher Servers" , "/world" , "gopher.floodgap.com" , 70 } ,
{ DIR , "Phlog aggregator" , "/bongusta/" , "i-logout.cz" , 70 } ,
{ DIR , "Another Phlog aggregator" , "/moku-pona" , "gopher.black" , 70 } ,
{ DIR , "Lobste.rs mirror" , "/users/julienxx/Lobste.rs" , "sdf.org" , 70 } ,
{ DIR , "jstg gopher" , "/users/jstg/" , "sdf.org" , 70 } ,
{ DIR , "phlogs" , "/phlogs/" , "sdf.org" , 70 } ,
{ DIR , "some stuff" , "" , "sdf.org" , 70 } ,
{ INFO , "" } ,
{ INFO , "Wisdom of the day:" } ,
{ INFO , function()
local res = {}
local q = io.popen(config.quotes,"r")
if q ~= nil then
for line in q:lines() do
table.insert(res,line)
end
q:close()
else
table.insert(res,"A witty saying goes here")
table.insert(res," -- Anon")
end
return res
end },
{ FILE , "robots.txt-because we can" , "/robots.txt" } ,
}
-- ***********************************************************************
-- Usage: handler.display_page(page)
-- Desc: Construct a gopher page
-- Input: page (array of links) - see example of top_page
-- ***********************************************************************
function display_page(page)
local size = 0
for _,line in ipairs(page) do
local t = line[1](line)
size = size + #t
io.stdout:write(line[1](line))
end
io.stdout:write(".\r\n")
return size + 3
end
-- ***********************************************************************
-- Usage: handler.display_request(selector,request)
-- Desc: Take a request and handle it
-- Input: selector (table) (see below for an example)
-- request (string) gopher request
-- ***********************************************************************
function display_request(selector,request)
for regex,code in pairs(selector) do
local pattern = request:match(regex)
if pattern then
return code(pattern)
end
end
syslog('error',"%q not found",request)
local err = ERROR { ERROR , "Not found" , request }
io.stdout:write(err,".\r\n")
return #err + 3
end
-- ***********************************************************************
-- Usage: handler.display_index_file(fname)
-- Desc: Display a gopher index file via gopher
-- Input: fname (string) filename
-- ***********************************************************************
local parsetype = lpeg.P"\t"
* lpeg.C(char^0) * lpeg.P"\t" -- type
* lpeg.C(char^0) * lpeg.P"\t" -- display
* lpeg.C(char^0) -- selector
function display_index_file(fname)
local page = {}
for line in io.lines(fname) do
if line == "" or line:match("^[^%c]") then
table.insert(page, { INFO , line })
else
local ftype,display,selector = parsetype:match(line)
table.insert(page , { FORMATS[ftype] , display , selector })
end
end
return display_page(page)
end
-- ***********************************************************************
-- Usage: handler.display_text_file(fname)
-- Desc: Display a text file via gopher
-- Input: fname (string) filename
-- ***********************************************************************
function display_text_file(fname)
local size = 0
for line in io.lines(fname) do
if line:match("^%.") then
io.stdout:write(".")
size = size + 1
end
io.stdout:write(line,"\r\n")
size = size + #line + 2
end
return size
end
-- ***********************************************************************
-- Usage: handler.display_fs_object(acl,selector,req,path)
-- Desc: Display a directory listing of files (path, then files)
-- Input: acl (table) ACL list
-- selector (string) base gopher selector
-- req (string) directory from gopher request
-- path (string) absolute directory
-- ***********************************************************************
function display_fs_object(acl,selector,req,path)
if not path_okay(acl,req) then
local err = ERROR { ERROR , "Not found" , req }
io.stdout:write(err, ".\r\n")
return #err + 3
end
local info = fsys.stat(path)
if info == nil then
local err = ERROR { ERROR , "Not found" , req }
io.stdout:write(err, ".\r\n")
return #err + 3
end
if info.mode.type == 'file' then
return display_text_file(path)
elseif info.mode.type ~= 'dir' then
local err = ERROR { ERROR , "Not found" , req }
io.stdout:write(err,".\r\n")
return #err + 3
end
local directories = {}
local files = {}
for file in fsys.dir(path) do
if path_okay(acl,file) then
info = fsys.stat(path .. "/" .. file)
if info.mode.type == 'file' then
table.insert(files,file)
elseif info.mode.type == 'dir' then
table.insert(directories,file)
end
end
end
table.sort(directories)
table.sort(files)
if req ~= "" then
req = req .. "/"
end
local size = 0
for i = 1 , #directories do
local dir = DIR { DIR , directories[i] , selector .. req .. directories[i] }
io.stdout:write(dir)
size = size + #dir
end
for i = 1 , #files do
local file = FILE { FILE , files[i] , selector .. req .. files[i] }
io.stdout:write(file)
size = size + #file
end
io.stdout:write(".\r\n")
return size + 3
end
-- ***********************************************************************
-- This table maps request selectors to functions to handle them. The keys
-- are patterns the request is matched against---first match wins, so order
-- accordingly.
-- ***********************************************************************
local selectors =
{
['^/robots%.txt$'] = function()
io.stdout:write("User-agent: *\r\nDisallow:\r\n")
return 26
end,
['^robots%.txt$'] = function()
io.stdout:write("User-agent: *\r\nDisallow:\r\n")
return 26
end,
['^/phlog.gopher$'] = function()
return display_text_file(config.files .. "/phlog.gopher")
end,
['^phlog.gopher$'] = function()
return display_text_file(config.files .. "/phlog.gopher")
end,
['^caps%.txt$'] = function()
return display_text_file(config.files .. "/caps.txt")
end,
['^/caps%.txt$'] = function()
return display_text_file(config.files .. "/caps.txt")
end,
['^About%:Server$'] = function()
return display_text_file(config.files .. "/about-server.txt")
end,
['^About%:Me$'] = function()
return display_text_file(config.files .. "/about-me.txt")
end,
['^Boston%:Src%:(.*)'] = function(req)
return display_fs_object(
ACL,
"Boston:Src:",
req,
"/home/spc/source/boston/" .. req
)
end,
['^CGI%:Src%:(.*)'] = function(req)
return display_fs_object(
ACL,
"CGI:Src:",
req,
"/home/spc/source/cgi/" .. req
)
end,
['^Phlog%:?(.*)'] = function(req)
local size
local data,text = blog.display(FORMATS,req)
if not data then
return 0
elseif type(data) == 'table' then
return display_page(blog.display(FORMATS,req))
elseif type(data) == 'string' then
io.stdout:write(data)
return #data
elseif io.type(data) == 'file' then
if text then
size = 0
for line in data:lines() do
io.stdout:write(line,"\r\n")
size = size + #line + 2
end
else
local pic = data:read("*a")
size = #pic
io.stdout:write(pic)
end
data:close()
return size
else
local data = tostring(data) -- luacheck: ignore
io.stdout:write(data)
return #data
end
end,
['^Gopher%:Src%:(.*)'] = function(req)
return display_fs_object(ACL,"Gopher:Src:",req,"/home/spc/source/gopher-blog/" .. req)
end,
['^Bible%:(.*)'] = function(req)
if req == "" then
return display_index_file(config.files .. "/electric-king-james.index")
else
return bible.handle(req)
end
end,
['^Movie%:(.*)'] = function(req)
return movie.handler(req)
end,
['^GET%s+.*'] = get.handler,
['^HEAD%s+.*'] = get.handler,
['^POST%s+.*'] = get.handler,
['^PUT%s+.*'] = get.handler,
['^DELETE%s+.*'] = get.handler,
['^CONNECT%s+.*'] = get.handler,
['^OPTIONS%s+.*'] = get.handler,
['^TRACE%s+.*'] = get.handler,
['^BREW%s+.*'] = get.handler, -- RFC-2324, in case people get cute
['^PROPFIND%s+.*'] = get.handler,
['^WHEN%s+.*'] = get.handler,
}
-- ***********************************************************************
-- Usage: handler.main(remote)
-- Desc: Main gopher request handler
-- Input: remote (userdata/address) remote address
-- Note: This does not return, but exits the process
-- ***********************************************************************
function main(remote)
local request = io.stdin:read("*l")
if request then
request = request:gsub("\013","")
else
io.stdout:write(ERROR { ERROR , "Bad request" , "" },"\r\n")
process.exit(0)
end
local okay
local size
if request == "" or request == "/" then
okay,size = pcall(display_page,top_page)
else
okay,size = pcall(display_request,selectors,request)
end
if not okay then
syslog('error',"host=%s request=%q err=%q",remote.addr,request,size)
else
syslog('info',"host=%s request=%q size=%d",remote.addr,request,size)
end
process.exit(0)
end
-- ***********************************************************************
-- Usage: handler.init()
-- Desc: Initialize the handler module.
-- ***********************************************************************
function init()
if config.interface.port == 70 then
config.url = string.format(
"gopher://%s/",
config.interface.hostname
)
else
config.url = string.format(
"gopher://%s:%d/",
config.interface.hostname,
config.interface.port
)
end
end
-- ***********************************************************************
if _VERSION >= "Lua 5.2" then
return _ENV
end