Initial implementation.
This commit is contained in:
parent
ed73edb430
commit
764cae3cdd
2
LICENSE
2
LICENSE
|
@ -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:
|
||||||
|
|
47
README.md
47
README.md
|
@ -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.
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue