-- *********************************************************************** -- -- 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 . -- -- Comments, questions and criticisms can be sent to: sean@conman.org -- -- ======================================================================= -- -- 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 .