💾 Archived View for gemini.conman.org › extensions › GLV-1 › handlers › mention.lua captured on 2023-06-16 at 16:46:21.

View Raw

More Information

⬅️ Previous capture (2023-01-29)

-=-=-=-=-=-=-

-- ************************************************************************
--
--    Another Gemini mention thingy
--    Copyright 2023 by Sean Conner.  All Rights Reserved.
--
--    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
--
-- ************************************************************************
-- luacheck: globals init handler
-- luacheck: ignore 611
-- gemini://gmi.bacardi55.io/gemlog/2022/02/27/my-take-on-gemlog-replies/

local process = require "org.conman.process"
local syslog  = require "org.conman.syslog"
local errno   = require "org.conman.errno"
local nfl     = require "org.conman.nfl"
local fsys    = require "org.conman.fsys"
local tls     = require "org.conman.nfl.tls"
local url     = require "org.conman.parsers.url"
local mime    = require "org.conman.parsers.mimetype"
local blog    = require "org.conman.app.mod_blog"
local uurl    = require "GLV-1.url-util"
local lpeg    = require "lpeg"
local string  = require "string"
local io      = require "io"

_ENV = {}

-- ************************************************************************

function init(conf)
  if not conf.pattern then
    return false,"Missing pattern rule"
  end
  
  if not conf.url then
    return false,"Missing require URL under mention control"
  end
  
  local loc = url:match(conf.url)
  if not loc then
    return false,"Bad URL"
  end
  
  conf.url = loc
  return true
end

-- ************************************************************************

local statparse do
  local Cc = lpeg.Cc
  local C  = lpeg.C
  local P  = lpeg.P
  local R  = lpeg.R
  local S  = lpeg.S
  
  local status   = P"1" * R"09" * Cc'input'    * Cc'required'  * Cc(true)
                 + P"2" * R"09" * Cc'okay'     * Cc'content'   * Cc(true)
                 + P"3" * R"09" * Cc'redirect' * Cc'temporary' * Cc(true)
                 + P"4" * R"09" * Cc'error'    * Cc'temporary' * Cc(true)
                 + P"5" * R"09" * Cc'error'    * Cc'permanent' * Cc(true)
                 + P"6" * R"09" * Cc'auth'     * Cc'required'  * Cc(true)
  local infotype = S" \t"^1 * C(R" \255"^0)
                 + Cc"type/text; charset=utf-8"
  statparse      = status * infotype
end

-- ************************************************************************

local function checktarget(ios,target,pattern,doctitle)
  local line = ios:read("*l")
  if not line then
    return false
  end
  
  if not doctitle and line:match "^#%s*[^#]" then
    doctitle = line:match("^#%s*(.*)")
    return checktarget(ios,target,pattern,doctitle)
  end
  
  if not line:match"^=>" then return checktarget(ios,target,pattern,doctitle) end
  local u,title   = line:match("^=>%s*(%S+)%s+(.*)")
  
  if not u then
    u = line:match9("^=>%s*(%S+)")
    title = ""
  end
  
  local loc = url:match(u)
  if not loc                     then return checktarget(ios,target,pattern,doctitle) end
  if target.scheme ~= loc.scheme then return checktarget(ios,target,pattern,doctitle) end
  if target.host   ~= loc.host   then return checktarget(ios,target,pattern,doctitle) end
  if target.port   ~= loc.port   then return checktarget(ios,target,pattern,doctitle) end
  
  local year,month,day,part = loc.path:match(pattern)
  if not year then
    return checktarget(ios,target,pattern,doctitle)
  end
  
  return doctitle or title,year,month,day,part
end

-- ************************************************************************

local function gemini_fetch(u,location,target,pattern,rcount)
  local ios = tls.connect(location.host,location.port,nil,function(conf)
    conf:insecure_no_verify_name()
    conf:insecure_no_verify_time()
    conf:insecure_no_verify_cert()
    return conf:protocols "all"
  end)
  
  if not ios then
    return
  end
  
  if not ios:write(u,"\r\n") then
    ios:close()
    return
  end
  
  local statline = ios:read("*l")
  if not statline then
    ios:close()
    return
  end
  
  local system,_,_,info = statparse:match(statline)
  if not system then
    ios:close()
    return
  end
  
  if system == 'auth' then
    ios:close()
    return
    
  elseif system == 'redirect' then
    ios:close()
    if rcount == 5 then
      return
    end
    local where = url:match(info)
    local new   = uurl.merge(location,where)
    local newloc = uurl.toa(new)
    
    return gemini_fetch(ios,newloc,target,rcount + 1)
    
  elseif system == 'okay' then
    info = mime:match(info)
    if not info or info.type ~= 'text/gemini' then
      ios:close()
      return
    end
    local title,year,month,day,part = checktarget(ios,target,pattern)
    ios:close()
    return title,year,month,day,part
    
  else
    ios:close()
    return
  end
end

-- ************************************************************************

function handler(conf,_,loc,_,ios)
  local function append(file,location,title)
    for line in file:lines() do
      local u = line:match("^(%S+)")
      if u == location then
        return
      end
    end
    file:write(location,"\t",title,"\n")
  end
  
  if not loc.query or loc.query == "" then
    ios:write("10 URL to send\r\n")
    return 10
  end
  
  loc.query = loc.query:gsub("%%%x%x",
                        function(c)
                          return string.char(tonumber(c:sub(2),16))
                        end)
                        
  local su = url:match(loc.query)
  local tu = conf.url
  
  if not su then
    ios:write("59 Bad request\r\n")
    return 59
  end
  
  local title,year,month,day,part = gemini_fetch(loc.query,su,tu,conf.pattern,1)
  if not title then
    ios:write("59 Bad request\r\n")
    return 59
  end
  
  local file = blog.filename {
        start    = { year = year , month = month , day = day } ,
        filename = string.format("%d.webmention",part)
  }
  local f,err = io.open(file,"a+")
  if not f then
    syslog('error',"%s: %s",file,err)
    ios:write("59 bad request\r\n")
    return 59
  end
  
  -- -----------------------------------------------------------------
  -- In order to avoid blocking the entire process, we need to call
  -- fsys._lock() with the non-blocking option, and if we would lock, just
  -- yield our coroutine and try again.  Yes, this may burn some CPU cycles,
  -- but it shouldn't be that bad.  I can always think on this if it does
  -- become an issue.
  -- -----------------------------------------------------------------
  
  local function lock()
    local okay,err1 = fsys._lock(f,'write',true)
    if okay then return okay end
    if err1 == errno.EACCESS or err1 == errno.EAGAIN then
      nfl.schedule(coroutine.running())
      coroutine.yield()
      return lock()
    else
      return okay,err1
    end
  end
  
  local okay,err2 = lock()
  if not okay then
    syslog('error',"%s: %s",file,errno[err2])
    ios:write("40 Temporary failure\r\n")
    return 40
  end
  
  append(f,uurl.toa(su),title)
  fsys._lock(f,'release',true)
  f:close()
  	
  ios:write("20 text/plain\r\nIt's been accepted.  Thank you.\n")
  
  if conf.update then
    local child,err1 = process.fork()
    if not child then
      syslog('error',"fork() = %s",errno[err1])
    elseif child == 0 then
      process.exec(conf.update.cmd,conf.update.args,conf.update.env)
      process.exit(1)
    else
      local info,err3 = process.wait(child)
      if not info then
        syslog('error',"wait() = %s",errno[err3])
      else
        if info.status ~= 'normal' or info.rc ~= 0 then
          syslog('error',"%s: status=%q description=%q rc=%d",info.status,info.description,info.rc)
        end
      end
    end
  end
  
  if conf.email then
    local mail = io.popen("/usr/sbin/sendmail " .. conf.email,"w")
    if mail then
      mail:write(
        string.format("From: <%s>\n",conf.email),
        string.format("To: <%s>\n",conf.email),
        "Subject: Gemini mention 2, sir!\n",
        "\n",
        string.format("From: %s\n",ios.__remote.addr),
        string.format("%s\n",loc.query),
        "\n",
        "Info: gemini://gmi.bacardi55.io/gemlog/2022/02/27/my-take-on-gemlog-replies/\n"
      )
      
      mail:close()
    end
  end
  
  return 20
end

-- ************************************************************************

return _ENV