index.gmi: give repo.or.cz url
[gemini.koplugin.git] / gemini.lua
blob72d45728c829d7041c9ef34306e38c218f2e71fa
1 -- Implementation of the Gemini protocol, following version 0.24.0 of the spec
2 -- gemini://geminiprotocol.net/docs/protocol-specification.gmi
4 local socket = require("socket")
5 local ssl = require("ssl")
6 local url = require("socket.url")
7 local sha256 = require("ffi/sha2").sha256
8 local _ = require("gettext")
9 local T = require("ffi/util").template
11 -- trusted_times_step: consider connections this far apart as separate enough to
12 -- reinforce TOFU-trust
13 local trusted_times_step = 3600
14 local default_port = "1965"
16 local gemini = {}
18 -- print url, stripping default port and, optionally, "gemini://"
19 function gemini.showUrl(u, strip_gemini_scheme)
20 local copy = {}
21 local purl = u
22 if type(u) == "string" then
23 purl = url.parse(u)
24 if purl == nil then
25 return u
26 end
27 end
28 for k,v in pairs(purl) do
29 copy[k] = v
30 end
31 if copy.port == "1965" then
32 copy.port = nil
33 end
34 if strip_gemini_scheme and copy.scheme == "gemini" then
35 copy.scheme = nil
36 end
37 return url.build(copy):gsub("^//","",1)
38 end
40 function gemini.upUrl(u)
41 local up = url.absolute(u, ".")
42 if up == u then
43 up = url.absolute(u, "..")
44 end
45 return up
46 end
48 -- Does any necessary percent-escaping and stripping to make a
49 -- socket.url-parsed URL compliant with RFC 3986. '%' is encoded only where it
50 -- is not part of valid percent-encoding. Modifies purl in place.
51 function gemini.escape(purl)
52 local function escape(s, unescaped)
53 if s == nil then
54 return nil
55 end
56 -- hairy encoding of '%' where not followed by two hex chars:
57 s = s:gsub("%%%%","%%25%%"):gsub("%%%%","%%25%%"):gsub("%%%f[^%%%x]","%%25"):gsub("%%(%x)%f[%X]","%%25%1")
58 return s:gsub("[^"..unescaped.."%%]", function(c)
59 return string.format("%%%x",c:byte(1))
60 end)
61 end
62 local unreserved = "%w%-._~"
63 local subdelims = "!$&'()*+,;="
64 local pchar = unreserved..subdelims..":@"
65 purl.host = escape(purl.host, unreserved..subdelims)
66 purl.path = escape(purl.path, pchar.."/")
67 purl.query = escape(purl.query, pchar.."/?")
68 purl.port = purl.port and purl.port:gsub("[^%d]","")
69 end
71 local errTexts = {
72 ["40"] = _("temporary failure"),
73 ["41"] = _("server unavailable"),
74 ["42"] = _("CGI error"),
75 ["43"] = _("proxy error"),
76 ["44"] = _("too many requests"),
77 ["50"] = _("permanent failure"),
78 ["51"] = _("resource not found"),
79 ["52"] = _("resource gone"),
80 ["53"] = _("proxy request refused"),
81 ["59"] = _("bad request"),
82 ["60"] = _("client certificate required"),
83 ["61"] = _("client certificate not authorised"),
84 ["62"] = _("client certificate not valid"),
87 local ipv6_available = socket.tcp6() ~= nil
89 -- @table trust_store Opaque table to be stored and passed to subsequent calls
91 -- @param cafile CA certificates to use for CA-based trust. nil to disable.
92 -- WARNING: as of luasec-1.3.2, CA-based trust checking seems to be thoroughly
93 -- broken and should not be used:
94 -- https://github.com/lunarmodules/luasec/issues/161
96 -- @func check_trust_cb Callback to check for trust of a public key when we've
97 -- previously TOFU-trusted another;
98 -- parameters: fullhost, digest, trusted_times, expiry, cb;
99 -- callback should call cb("always") if the new pubkey should be added to
100 -- the trust store (replacing the old one),
101 -- or cb("once") to proceed with the connection without trusting the pubkey,
102 -- or else cb() to abort connection.
104 -- @func trust_modified_cb Callback called without params when trust_store is
105 -- modified (e.g. so it can be updated on disk)
107 -- @func success_cb Callback called on 20 (success) response;
108 -- parameters: peer_qfile, mimetype, params, cert_info;
109 -- peer_qfile is a quasi-file representing the body,
110 -- supporting :close, :read("l"), :read("a"), and :read(n), with read
111 -- calls returning nil,"aborted" if the connection is aborted via info_cb.
113 -- @func error_cb Callback called on connection error or error codes other
114 -- than 20;
115 -- parameters: errText, major, minor, meta;
116 -- errText describes the error; the rest are nil except on error code [13456],
117 -- then major and minor are the one-digit strings of the error code,
118 -- and meta is any text received in the response.
119 -- Should return nil.
121 -- @func info_cb Optional callback to present the progress message
122 -- given as its parameter. Return false to abort the connection.
124 -- @bool confirm_new_tofu Call check_trust_cb (with trusted_times = 0)
125 -- before trusting a key for a new host.
127 -- @string proxy Optional host to proxy request via.
129 -- @string titan_upload_body Optional body for upload via the Titan protocol,
130 -- following gemini://transjovian.org/titan/page/The%20Titan%20Specification
131 function gemini.makeRequest(u, key, cert, cafile, trust_store, check_trust_cb, trust_modified_cb, success_cb, error_cb, info_cb, confirm_new_tofu, proxy, titan_upload_body)
132 info_cb = info_cb or function() return true end
133 local purl = url.parse(u, {scheme = "gemini", port = default_port})
134 if not purl then
135 return error_cb(T(_("Failed to parse URL: %1"), u))
137 if purl.port == default_port then
138 purl.port = nil
140 if purl.scheme == "gemini" then
141 purl.fragment = nil
142 purl.userinfo = nil
143 purl.password = nil
144 if not purl.path or purl.path == "" then
145 purl.path = "/"
148 gemini.escape(purl)
149 u = url.build(purl)
151 if #u > 1024 then
152 return error_cb(T(_("URL too long: %1"), u))
155 local openssl_options = {"all", "no_tlsv1", "no_tlsv1_1"}
156 if key then
157 -- Do not allow connecting with a client certificate using TLSv1.2,
158 -- which does not encrypt the certificate.
159 -- (The gemini spec only says that we should warn the user before
160 -- doing so, but that seems not to be possible with luasec.)
161 table.insert(openssl_options, "no_tlsv1_2")
163 local context, err = ssl.newcontext({
164 mode = "client",
165 protocol = "any",
166 options = openssl_options,
167 cafile = cafile,
168 key = key,
169 certificate = cert,
171 if not context then
172 return error_cb(T(_("Error initialising TLS context: %1"), err))
175 local peer
176 if ipv6_available then
177 peer, err = socket.tcp()
178 else
179 peer, err = socket.tcp4()
181 if not peer then
182 return error_cb(T(_("Error initialising TCP: %1"), err))
185 local pretty_url = gemini.showUrl(u, true)
186 local function info(stage, fast)
187 return info_cb(T("%1\n%2...", pretty_url, stage), fast)
189 if not info("Connecting") then
190 return
193 local timeout = 1 -- not too short, because each call to info_cb costs 100ms
194 peer:settimeout(timeout)
196 local function with_timeouts(cb, stage, errmsg_pat, fast)
197 while true do
198 local ret, e = cb()
199 if ret then
200 return ret
202 if e == "closed" then
203 return false
204 elseif e == "timeout" or e == "wantread" or e == "wantwrite" then
205 while true do
206 if not info(stage, fast) then
207 return false, "aborted"
209 fast = true
210 local __, ___, sock_err = socket.select(
211 e == "wantread" and {peer} or nil,
212 (e == "timeout" or e == "wantwrite") and {peer} or nil, 1)
213 if sock_err == nil then
214 break
215 elseif sock_err ~= "timeout" then
216 return error_cb(T(error_cb, sock_err))
219 else
220 return error_cb(T(errmsg_pat, e))
224 local function get_send_cb(data)
225 local i = 0
226 return function()
227 local ret, e
228 ret, e, i = peer:send(data, i+1)
229 return ret, e
232 local function get_recv_cb(p)
233 local acc
234 return function()
235 local ret, e, partial = peer:receive(p)
236 if ret then
237 return (acc or "") .. ret
238 else
239 if partial then
240 acc = (acc or "") .. partial
242 if e == "closed" and acc then
243 return acc
245 return ret, e
250 local host, port
251 if proxy then
252 host, port = proxy:match("^([^:]*):(%d+)")
253 if not host then
254 host = proxy
255 port = default_port
257 else
258 host = purl.host
259 port = tonumber(purl.port or default_port)
261 local function get_connect_cb()
262 local done
263 return function()
264 if done then
265 return true
266 else
267 done = true
268 return peer:connect(host, port)
272 if not with_timeouts(get_connect_cb(),
273 _("Connecting"), _("Error connecting to peer: %1"), true)
274 then
275 return peer:close()
278 peer, err = ssl.wrap(peer, context)
279 if not peer then
280 return error_cb(T(_("Error on ssl.wrap: %1"), err))
282 peer:settimeout(timeout)
283 peer:sni(host)
285 if not with_timeouts(function() return peer:dohandshake() end,
286 _("Handshaking"), _("Error on handshake: %1"))
287 then
288 return peer:close()
291 local fullhost
292 if proxy then
293 fullhost = proxy
294 else
295 fullhost = purl.host
296 if purl.port then
297 fullhost = fullhost .. ":" .. purl.port
301 local peer_cert = peer:getpeercertificate()
302 if not peer_cert then
303 peer:close()
304 return error_cb(_("Failed to obtain peer certificate"))
306 local pk_hash = sha256(peer_cert:pubkey())
307 local digest = peer_cert:digest("sha256")
308 local expiry = peer_cert:notafter()
310 local function do_connection(trusted)
311 if not with_timeouts(get_send_cb(u.."\r\n"),
312 _("Requesting"), _("Error sending request: %1"))
313 then
314 return peer:close()
317 local status
318 if titan_upload_body then
319 -- XXX hack: give server a second to respond with an error to our
320 -- upload attempt. If we just start sending, the connection will
321 -- be closed and then it seems we can't receive the early
322 -- response.
323 status, err = peer:receive("*l")
324 if err and err ~= "wantread" and err ~= "wantwrite" then
325 error_cb(T(_("Error receiving early response status: %1"), err))
326 return peer:close()
329 if not status then
330 local __, aborted = with_timeouts(get_send_cb(titan_upload_body),
331 _("Uploading"), _("Error during upload: %1"))
332 if aborted then
333 return peer:close()
338 -- Note: "*l" reads a line terminated either with \n or \r\n or
339 -- EOF, whereas the Gemini header must be terminated with \r\n.
340 -- However, no \n is allowed before the \r\n, so the only effect
341 -- is to accept some invalid responses.
342 status = status or with_timeouts(get_recv_cb("*l"),
343 _("Receiving header"), _("Error receiving response status : %1"))
344 if not status then
345 return peer:close()
348 local major, minor, post = status:match("^([1-6])([0-9])(.*)")
349 if not major or not minor then
350 error_cb(T(_("Invalid response status line: %1"), status:sub(1,64)))
351 return peer:close()
353 local meta = post:match("^ (.*)") or ""
355 if major == "2" then
356 local token_chars = "[a-zA-Z0-9!#$%%&'*+-.^_`{|}-]" -- from RFC 2045
357 local mimetype, params_str = meta:match("^("..token_chars.."+/"..token_chars.."+)(.*)")
358 mimetype = mimetype or "text/gemini"
359 local params = {}
360 while params_str and params_str ~= "" do
361 local param, rest = params_str:match("^;%s*("..token_chars.."+=\"[^\"]*\")(.*)")
362 if not param then
363 param, rest = params_str:match("^;%s*("..token_chars.."+="..token_chars.."+)(.*)")
365 if not param then
366 -- ignore unparseable parameters
367 break
369 params_str = rest
370 table.insert(params, param)
373 local peer_qfile = {}
374 function peer_qfile:read(x)
375 x = (type(x) == "string" and x:match("^[al]$") and "*"..x)
376 or (type(x) == "number" and x)
377 return with_timeouts(get_recv_cb(x),
378 _("Receiving body"), _("Error receiving body: %1"))
380 function peer_qfile:close()
381 peer:close()
383 local cert_info = {
384 fp = digest,
385 expiry = expiry,
386 trusted_times = trusted and trusted.trusted_times or 0,
387 ca = trusted and trusted.ca
389 success_cb(peer_qfile, mimetype, params, cert_info)
390 else
391 peer:close()
392 if tonumber(major) < 1 or tonumber(major) > 6 then
393 error_cb(_("Server returns invalid error code."))
394 else
395 local errText = errTexts[major..minor]
396 if not errText then
397 errText = errTexts[major.."0"]
399 error_cb(errText, major, minor, meta)
404 local function set_trust(times)
405 trust_store[fullhost] = {
406 pk_hash = pk_hash,
407 digest = digest,
408 expiry = expiry,
409 trusted_times = 1,
410 last_trust_time = os.time(),
412 trust_modified_cb()
415 if cafile then
416 if peer:getpeerverification() then
417 local ca_cert
418 for _i, c in ipairs( peer:getpeerchain() ) do
419 ca_cert = c
421 return do_connection({ ca = ca_cert:issuer() })
425 local function trust_cb_cb(user_trust)
426 if user_trust == "always" then
427 set_trust()
428 do_connection(trust_store[fullhost])
429 elseif user_trust == "once" then
430 do_connection()
431 else
432 peer:close()
436 local trusted = trust_store[fullhost]
437 if not trusted then
438 -- Trust On First Use
439 if confirm_new_tofu then
440 check_trust_cb(fullhost, digest, 0, "", trust_cb_cb)
441 else
442 trust_cb_cb("always")
444 else
445 if trusted.pk_hash == pk_hash then
446 local now = os.time()
447 if now - trusted.last_trust_time > trusted_times_step then
448 trusted.trusted_times = trusted.trusted_times + 1
449 trusted.last_trust_time = now
450 trust_modified_cb()
452 if trusted.digest ~= digest then
453 -- Extend trust to update cert with same public key.
454 trusted.digest = digest
455 trusted.expiry = expiry
456 trust_modified_cb()
458 do_connection(trusted)
459 else
460 check_trust_cb(fullhost, digest, trusted.trusted_times, trusted.expiry, trust_cb_cb)
464 return gemini