diff --git a/LICENSE b/LICENSE index c2e480e..3acdfcf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) . All rights reserved. +Copyright (c) 2019 . All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md index 8869cff..7b86696 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,46 @@ -# gemini-demo-2 +# gemini-demo-1 -Minimal but usable interactive Gemini client in < 100 LOC of Lua \ No newline at end of file +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. diff --git a/gemini-demo.lua b/gemini-demo.lua new file mode 100644 index 0000000..c85a850 --- /dev/null +++ b/gemini-demo.lua @@ -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