Initial implementation.

This commit is contained in:
Solderpunk 2019-08-11 23:17:22 +03:00
parent ed73edb430
commit 764cae3cdd
3 changed files with 149 additions and 3 deletions

View File

@ -1,4 +1,4 @@
Copyright (c) <year> <owner> . All rights reserved. Copyright (c) 2019 <solderpunk@sdf.org> . All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met: are permitted provided that the following conditions are met:

View File

@ -1,3 +1,46 @@
# gemini-demo-2 # gemini-demo-1
Minimal but usable interactive Gemini client in < 100 LOC of Lua Minimal but usable interactive Gemini client in < 100 LOC of Lua.
Depends upon:
* [LuaSocket](http://w3.impa.br/~diego/software/luasocket/)
* [LuaSec](https://github.com/brunoos/luasec)
* [Microlight](https://stevedonovan.github.io/microlight/)
## Rationale
One of the original design criteria for the Gemini protocol was that
"a basic but usable (not ultra-spartan) client should fit comfortably
within 50 or so lines of code in a modern high-level language.
Certainly not more than 100". This client was written to gauge how
close to (or far from!) that goal the initial rough specification is.
## Capabilities
This crude but functional client:
* Has a minimal interactive interface for "Gemini maps"
* Will print plain text in any encoding if it is properly declared in
the server's response header
* Will follow redirects
* Will report errors
* Does NOT DO ANY validation of TLS certificates
Non-text files are not yet handled.
It's a *snug* fit in 100 lines, but it's possible. A 50 LOC client
would need to be much simpler.
## Usage
Run the script and you'll get a prompt. Type a Gemini URL (the scheme
is implied, so simply entering e.g. `gemini.conman.org` will work) to
visit a Gemini location.
If a Gemini menu is visited, you'll see numeric indices for links, ala
VF-1 or AV-98. Type a number to visit that link.
There is very crude history: you can type `b` to go "back".
Type `q` to quit.

103
gemini-demo.lua Normal file
View File

@ -0,0 +1,103 @@
socket = require("socket")
socket.url = require("socket.url")
ssl = require("ssl")
ml = require('ml')
ssl_params = {
mode = "client",
protocol = "tlsv1_2"
}
links = {}
history = {}
while true do
::main_loop::
io.write("> ")
cmd = io.read()
if string.lower(cmd) == "q" then
io.write("Bye!\n")
break
elseif tonumber(cmd) ~= nil then
url = links[tonumber(cmd)]
elseif string.lower(cmd) == "b" then
-- Yes, twice
url = table.remove(history)
url = table.remove(history)
else
url = cmd
end
-- Add scheme if missing
if string.find(url, "://") == nil then
url = "gemini://" .. url
end
-- Add empty path if needed
if string.find(string.sub(url, 10, -1), "/") == nil then
url = url .. "/"
end
::parse_url::
parsed_url = socket.url.parse(url)
-- Open connection
conn = socket.tcp()
ret, str = conn:connect(parsed_url.host, 1965)
if ret == nil then
io.write(str) goto main_loop
end
conn, err = ssl.wrap(conn, ssl_params)
if conn == nil then
io.write(err) goto main_loop
end
conn:dohandshake()
-- Send request
conn:send(url .. "\r\n")
-- Parse response header
header = conn:receive("*l")
status, meta = table.unpack(ml.split(header, "%s+", 2))
-- Handle sucessful response
if string.sub(status, 1, 1) == "2" then
if meta == "text/gemini" then
-- Handle Geminimap
links = {}
while true do
line, err = conn:receive("*l")
if line ~= nil then
if string.sub(line,1,2) == "=>" then
line = string.sub(line,3,-1) -- Trim off =>
line = string.gsub(line,"^%s+","") -- Trim spaces
link_url, text = table.unpack(ml.split(line, "%s+", 2))
if text == nil then text = link_url end
table.insert(links, socket.url.absolute(url, link_url))
io.write("[" .. #links .. "] " .. text .. "\n")
else
io.write(line .. "\n")
end
else
break
end
end
elseif string.sub(meta, 1, 5) == "text/" then
-- Print text
while true do
line, err = conn:receive("*l")
if line ~= nil then
io.write(line .. "\n")
else
break
end
end
end
-- Handle redirects
elseif string.sub(status, 1, 1) == "3" then
url = socket.url.absolute(url, meta)
goto parse_url
-- Handle errors
elseif string.sub(status, 1, 1) == "4" or string.sub(status, 1, 1) == "5" then
io.write("Error: " .. meta)
elseif string.sub(status, 1, 1) == "6" then
io.write("Client certificates not supported.")
else
io.write("Invalid response from server.")
end
table.insert(history, url)
end