[spec] The Tragedy of &

Sean Conner sean at conman.org
Sat Jan 30 23:59:47 GMT 2021
It was thus said that the Great Gary Johnson once stated:
> 
> The issue I'm raising is that there appears to be no way to pass more
> than one piece of information at a time in our query strings. This has a
> very significant impact on any writers of CGI scripts, which is how many
> Gemini servers allow users to add dynamic pages to their capsules.
> 
> But why, you ask?
> 
> Because each CGI script is available at a particular file path and
> therefore additional path segments can't be used to pass information to
> them. They have to get their inputs from the query string.

  [ snip ]

> It means essentially that CGI scripts are currently second-class
> citizens, and the only people who can write dynamic capsules are server
> authors (or people willing to hack on server code). This is because
> encoding information using path segments requires injecting custom
> routing table code into the server's request handler.

  Not if the CGI interface is properly written.  All I had to do was write
this CGI script and drop it into my tests directory [1]:

	gemini://gemini.conman.org/test/pathseg.cgi

It uses only three of the RFC-3875 defined variables, QUERY_STRING,
SCRIPT_NAME and PATH_INFO to do all the work.  The script will ask for three
fields and then present a final page with all three fields.  But the script
will only work if all three variables are defined per RFC-3975 (PATH_INFO is
the tricky one).

  Yes, it's a bit ugly and yes, it's a second class citizen and yes, it
requires a proper CGI module to work, but it can be done without the
configuration you think it does.  The script just simply appends each input
field as the path, so if you enter

	and a one
	and a two
	skidoosh

as the values, the final URL will be:

	gemini://gemini.conman.org/test/pathseg.cgi/and%20a%20one/and%20a%20two/skidoosh

  Yes, I could have done a bit more processing, naming each segment:

	/test/pathseg.cgi/name=and%20a%20one/age=and%20a%20two/action=skidoosh

but I was lazy and wanted to just do a proof-of-concept here.
	
> If you can write a CGI script that can correctly associate INPUT
> responses with their intended variables, please share it. I suspect it
> would be quite educational.

  I have added it [3].

  -spc

[1]	I gave the script a .cgi extension just to drive the point
	home---for my server, GLV-1.12556 [2], the extension of a CGI script
	doesn't matter at all.

[2]	https://github.com/spc476/GLV-1.12556

[3]	Here you go.  It's in Lua, but it's easy going except for the first
	bit which is a bit of broilerplate I needed for encoding and
	decoding various strings.  The main logic is marked though, so you
	can skip the first section.

#!/usr/bin/env lua

-- ************************************************************************
-- Decoding and Encoding crap, not much to see here, citizen!  Move along!
-- ************************************************************************

local lpeg    = require "lpeg"
local xdigit  = lpeg.locale().xdigit
local char    = lpeg.P"%" * lpeg.C(xdigit * xdigit)
              / function(c)
                  return string.char(tonumber(c,16))
                end
              + lpeg.P"+" / " "
              + lpeg.P(1)
local decode_query = lpeg.Cs(char^1)

local function tohex(c)
  return string.format("%%%02X",string.byte(c))
end

local unsafe     = lpeg.P" "  / "%%20"
                 + lpeg.P"#"  / "%%23"
                 + lpeg.P"%"  / "%%25"
                 + lpeg.P"<"  / "%%3C"
                 + lpeg.P">"  / "%%3E"
                 + lpeg.P"["  / "%%5B"
                 + lpeg.P"\\" / "%%5C"
                 + lpeg.P"]"  / "%%5D"
                 + lpeg.P"^"  / "%%5E"
                 + lpeg.P"{"  / "%%7B"
                 + lpeg.P"|"  / "%%7C"
                 + lpeg.P"}"  / "%%7D"
                 + lpeg.P'"'  / "%%22"
                 + lpeg.R("\0\31","\127\255") / tohex
local char_path  = lpeg.P"?"  / "%%3F"
                 + unsafe
                 + lpeg.P(1)
local esc_path   = lpeg.Cs(char_path^0)

-- ************************************************************************
-- The main script starts here
-- ************************************************************************

local query       = os.getenv("QUERY_STRING")
local script_name = os.getenv("SCRIPT_NAME")
local pathinfo    = os.getenv("PATH_INFO")

if not pathinfo and query == "" then
  io.stdout:write("Status: 10\n")
  io.stdout:write("Content-Type: Input field\n")
  io.stdout:write("\n")
  os.exit(0,true)
end

if not pathinfo then
  query = decode_query:match(query)
  query = esc_path:match(query)
  io.stdout:write("Status: 30\n")
  io.stdout:write(string.format("Location: %s/%s\n",script_name,query))
  io.stdout:write("\n")
  os.exit(0,true)
end

if pathinfo:match("^/[^/]*/[^/]*/[^/]*") then
  local f1,f2,f3 = pathinfo:match("^/([^/]*)/([^/]*)/([^/]*)")
  f1 = decode_query:match(f1)
  f2 = decode_query:match(f2)
  f3 = decode_query:match(f3)
  
  io.stdout:write("Status: 20\n")
  io.stdout:write("Content-Type: text/gemini\n")
  io.stdout:write("\n")
  io.stdout:write("The three fields you input:\n")
  io.stdout:write("\n")
  io.stdout:write(string.format("* %s\n",f1))
  io.stdout:write(string.format("* %s\n",f2))
  io.stdout:write(string.format("* %s\n",f3))
  io.stdout:write("\n")
  io.stdout:write(string.format("=> %s Try again\n",script_name))
  os.exit(0,true)
end

if query == "" then
  io.stdout:write("Status: 10\n")
  io.stdout:write("Content-Type: Input next field\n")
  io.stdout:write("\n")
  os.exit(0,true)
else
  query    = decode_query:match(query)
  query    = esc_path:match(query)
  pathinfo = esc_path:match(pathinfo)
  io.stdout:write("Status: 30\n")
  io.stdout:write(string.format("Location: %s%s/%s\n",script_name,pathinfo,query))
  io.stdout:write("\n")
  os.exit(0,true)
end


Archive home | About this archive