1 local socket
= require("socket")
2 local ssl
= require("ssl")
3 local url
= require("socket.url")
4 local sha256
= require("ffi/sha2").sha256
5 local _
= require("gettext")
6 local T
= require("ffi/util").template
8 -- trusted_times_step: consider connections this far apart as separate enough to
9 -- reinforce TOFU-trust
10 local trusted_times_step
= 3600
11 local default_port
= "1965"
15 -- print url, stripping default port and, optionally, "gemini://"
16 function gemini
.showUrl(u
, strip_gemini_scheme
)
19 if type(u
) == "string" then
25 for k
,v
in pairs(purl
) do
28 if copy
.port
== "1965" then
31 if strip_gemini_scheme
and copy
.scheme
== "gemini" then
34 return url
.build(copy
):gsub("^//","",1)
37 function gemini
.upUrl(u
)
38 local up
= url
.absolute(u
, ".")
40 up
= url
.absolute(u
, "..")
45 -- Does any necessary percent-escaping and stripping to make a
46 -- socket.url-parsed URL compliant with RFC 3986. '%' is encoded only where it
47 -- is not part of valid percent-encoding. Modifies purl in place.
48 function gemini
.escape(purl
)
49 local function escape(s
, unescaped
)
53 -- hairy encoding of '%' where not followed by two hex chars:
54 s
= s
:gsub("%%%%","%%25%%"):gsub("%%%%","%%25%%"):gsub("%%%f[^%%%x]","%%25"):gsub("%%(%x)%f[%X]","%%25%1")
55 return s
:gsub("[^"..unescaped
.."%%]", function(c
)
56 return string.format("%%%x",c
:byte(1))
59 local unreserved
= "%w%-._~"
60 local subdelims
= "!$&'()*+,;="
61 local pchar
= unreserved
..subdelims
..":@"
62 purl
.host
= escape(purl
.host
, unreserved
..subdelims
)
63 purl
.path
= escape(purl
.path
, pchar
.."/")
64 purl
.query
= escape(purl
.query
, pchar
.."/?")
65 purl
.port
= purl
.port
and purl
.port
:gsub("[^%d]","")
69 ["40"] = _("temporary failure"),
70 ["41"] = _("server unavailable"),
71 ["42"] = _("CGI error"),
72 ["43"] = _("proxy error"),
73 ["44"] = _("too many requests"),
74 ["50"] = _("permanent failure"),
75 ["51"] = _("resource not found"),
76 ["52"] = _("resource gone"),
77 ["53"] = _("proxy request refused"),
78 ["59"] = _("bad request"),
79 ["60"] = _("client certificate required"),
80 ["61"] = _("client certificate not authorised"),
81 ["62"] = _("client certificate not valid"),
84 local ipv6_available
= socket
.tcp6() ~= nil
86 -- @table trust_store Opaque table to be stored and passed to subsequent calls
88 -- @param cafile CA certificates to use for CA-based trust. nil to disable.
89 -- WARNING: as of luasec-1.3.2, CA-based trust checking seems to be thoroughly
90 -- broken and should not be used:
91 -- https://github.com/lunarmodules/luasec/issues/161
93 -- @func check_trust_cb Callback to check for trust of a public key when we've
94 -- previously TOFU-trusted another;
95 -- parameters: fullhost, digest, trusted_times, expiry, cb;
96 -- callback should call cb("always") if the new pubkey should be added to
97 -- the trust store (replacing the old one),
98 -- or cb("once") to proceed with the connection without trusting the pubkey,
99 -- or else cb() to abort connection.
101 -- @func trust_modified_cb Callback called without params when trust_store is
102 -- modified (e.g. so it can be updated on disk)
104 -- @func success_cb Callback called on 20 (success) response;
105 -- parameters: peer_qfile, purl, mimetype, params, cert_info;
106 -- peer_qfile is a quasi-file representing the body,
107 -- supporting :close, :read("l"), :read("a"), and :read(n), with read
108 -- calls returning nil,"aborted" if the connection is aborted via info_cb;
109 -- purl is the final url.parse parsed URI (after any redirects).
111 -- @func error_cb Callback called on connection error or error codes 1,4,5,6;
112 -- parameters: errText, purl, major, minor, meta;
113 -- errText describes the error; the rest are nil except on error code [1456],
114 -- then purl is the final url.parse parsed URI (after any redirects),
115 -- major and minor are the one-digit strings of the error code,
116 -- and meta is any text received in the response.
117 -- Should return nil.
119 -- @func perm_redir_cb Optional callback called on permanent redirect (31)
120 -- parameters: old_uri, new_uri
122 -- @func info_cb Optional callback to present the progress message
123 -- given as its parameter. Return false to abort the connection.
125 -- @bool confirm_new_tofu Call check_trust_cb (with trusted_times = 0)
126 -- before trusting a key for a new host.
128 -- @string proxy Optional host to proxy request via.
129 function gemini
.makeRequest(uri
, key
, cert
, cafile
, trust_store
, check_trust_cb
, trust_modified_cb
, success_cb
, error_cb
, perm_redir_cb
, info_cb
, confirm_new_tofu
, proxy
)
130 info_cb
= info_cb
or function() return true end
132 inner
= function(u
, num_redirects
)
133 local purl
= url
.parse(u
, {scheme
= "gemini", port
= default_port
})
134 if purl
.port
== default_port
then
137 if purl
.scheme
== "gemini" then
141 if not purl
.path
or purl
.path
== "" then
149 return error_cb(T(_("URL too long: %1"), u
))
152 local openssl_options
= {"all", "no_tlsv1", "no_tlsv1_1"}
154 -- Do not allow connecting with a client certificate using TLSv1.2,
155 -- which does not encrypt the certificate.
156 -- (The gemini spec only says that we should warn the user before
157 -- doing so, but that seems not to be possible with luasec.)
158 table.insert(openssl_options
, "no_tlsv1_2")
160 local context
, err
= ssl
.newcontext({
163 options
= openssl_options
,
169 return error_cb(T(_("Error initialising TLS context: %1"), err
))
173 if ipv6_available
then
174 peer
, err
= socket
.tcp()
176 peer
, err
= socket
.tcp4()
179 return error_cb(T(_("Error initialising TCP: %1"), err
))
182 local pretty_url
= gemini
.showUrl(u
, true)
183 local function info(stage
, fast
)
184 return info_cb(T("%1\n%2...", pretty_url
, stage
), fast
)
186 if not info("Connecting") then return end
188 local timeout
= 1 -- not too short, because each call to info_cb costs 100ms
189 peer
:settimeout(timeout
)
191 local function with_timeouts(cb
, stage
, errmsg_pat
, fast
)
197 if e
== "closed" then
199 elseif e
== "timeout" or e
== "wantread" or e
== "wantwrite" then
201 if not info(stage
, fast
) then
205 local __
, ___
, sock_err
= socket
.select(
206 e
== "wantread" and {peer
} or nil,
207 (e
== "timeout" or e
== "wantwrite") and {peer
} or nil, 1)
208 if sock_err
== nil then
210 elseif sock_err
~= "timeout" then
211 return error_cb(T(error_cb
, sock_err
))
215 return error_cb(T(errmsg_pat
, e
))
219 local function get_send_cb(data
)
223 ret
, e
, i
= peer
:send(data
, i
+1)
227 local function get_recv_cb(p
)
230 local ret
, e
, partial
= peer
:receive(p
)
232 return (acc
or "") .. ret
235 acc
= (acc
or "") .. partial
237 if e
== "closed" and acc
then
247 host
, port
= proxy
:match("^([^:]*):(%d+)")
254 port
= tonumber(purl
.port
or default_port
)
256 local function get_connect_cb()
263 return peer
:connect(host
, port
)
267 if not with_timeouts(get_connect_cb(),
268 _("Connecting"), _("Error connecting to peer: %1"), true, false, true)
271 peer
, err
= ssl
.wrap(peer
, context
)
273 return error_cb(T(_("Error on ssl.wrap: %1"), err
))
275 peer
:settimeout(timeout
)
278 if not with_timeouts(function() return peer
:dohandshake() end,
279 _("Handshaking"), _("Error on handshake: %1"))
288 fullhost
= fullhost
.. ":" .. purl
.port
292 local peer_cert
= peer
:getpeercertificate()
293 if not peer_cert
then
294 return error_cb(_("Failed to obtain peer certificate"))
296 local pk_hash
= sha256(peer_cert
:pubkey())
297 local digest
= peer_cert
:digest("sha256")
298 local expiry
= peer_cert
:notafter()
300 local function do_connection(trusted
)
301 if not with_timeouts(get_send_cb(u
.."\r\n"),
302 _("Requesting"), _("Error sending request: %1"))
305 local status
= with_timeouts(get_recv_cb("*l"),
306 _("Receiving header"), _("Error receiving response status : %1"))
307 if not status
then return end
309 local major
, minor
, post
= status
:match("^([1-6])([0-9])(.*)")
310 if not major
or not minor
then
311 return error_cb(T(_("Invalid response status line: %1", status
:sub(1,64))))
313 local meta
= post
:match("^ (.*)") or ""
316 local token_chars
= "[a-zA-Z0-9!#$%%&'*+-.^_`{|}-]" -- from RFC 2045
317 local mimetype
, params_str
= meta
:match("^("..token_chars
.."+/"..token_chars
.."+)(.*)")
318 mimetype
= mimetype
or "text/gemini"
320 while params_str
and params_str
~= "" do
321 local param
, rest
= params_str
:match("^;%s*("..token_chars
.."+=\"[^\"]*\")(.*)")
323 param
, rest
= params_str
:match("^;%s*("..token_chars
.."+="..token_chars
.."+)(.*)")
326 -- ignore unparseable parameters
330 table.insert(params
, param
)
333 local peer_qfile
= {}
334 function peer_qfile
:read(x
)
335 x
= (type(x
) == "string" and x
:match("^[al]$") and "*"..x
)
336 or (type(x
) == "number" and x
)
337 return with_timeouts(get_recv_cb(x
),
338 _("Receiving body"), _("Error receiving body: %1"), false, true)
340 function peer_qfile
:close()
346 trusted_times
= trusted
and trusted
.trusted_times
or 0,
347 ca
= trusted
and trusted
.ca
349 success_cb(peer_qfile
, purl
, mimetype
, params
, cert_info
)
353 if num_redirects
>= 5 then
354 return error_cb(_("Too many redirects."))
356 local new_uri
= url
.absolute(purl
, meta
)
357 if minor
== "1" and perm_redir_cb
then
358 perm_redir_cb(u
, new_uri
)
360 inner(new_uri
, num_redirects
+ 1)
361 elseif tonumber(major
) < 1 or tonumber(major
) > 6 then
362 error_cb(_("Server returns invalid error code."))
364 local errText
= errTexts
[major
..minor
]
366 errText
= errTexts
[major
.."0"]
368 error_cb(errText
, purl
, major
, minor
, meta
)
373 local function set_trust(times
)
374 trust_store
[fullhost
] = {
379 last_trust_time
= os
.time(),
385 if peer
:getpeerverification() then
387 for _i
, c
in ipairs( peer
:getpeerchain() ) do
390 return do_connection({ ca
= ca_cert
:issuer() })
394 local function trust_cb_cb(user_trust
)
395 if user_trust
== "always" then
397 do_connection(trust_store
[fullhost
])
398 elseif user_trust
== "once" then
405 local trusted
= trust_store
[fullhost
]
407 -- Trust On First Use
408 if confirm_new_tofu
then
409 check_trust_cb(fullhost
, digest
, 0, "", trust_cb_cb
)
411 trust_cb_cb("always")
414 if trusted
.pk_hash
== pk_hash
then
415 local now
= os
.time()
416 if now
- trusted
.last_trust_time
> trusted_times_step
then
417 trusted
.trusted_times
= trusted
.trusted_times
+ 1
418 trusted
.last_trust_time
= now
421 if trusted
.digest
~= digest
then
422 -- Extend trust to update cert with same public key.
423 trusted
.digest
= digest
424 trusted
.expiry
= expiry
427 do_connection(trusted
)
429 check_trust_cb(fullhost
, digest
, trusted
.trusted_times
, trusted
.expiry
, trust_cb_cb
)