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