SMOLNET PORTAL home about changes
-- ***********************************************************************
--
-- 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: 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
.
Response: text/plain
Original URLgopher://gopher.conman.org/0Obsolete%3AGopher%3ASrc%3Ahandler.lua
Content-Typetext/plain; charset=utf-8