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,
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