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
, "..")
47 ["40"] = _("temporary failure"),
48 ["41"] = _("server unavailable"),
49 ["42"] = _("CGI error"),
50 ["43"] = _("proxy error"),
51 ["44"] = _("too many requests"),
52 ["50"] = _("permanent failure"),
53 ["51"] = _("resource not found"),
54 ["52"] = _("resource gone"),
55 ["53"] = _("proxy request refused"),
56 ["59"] = _("bad request"),
57 ["60"] = _("client certificate required"),
58 ["61"] = _("client certificate not authorised"),
59 ["62"] = _("client certificate not valid"),
62 -- @table trust_store Opaque table to be stored and passed to subsequent calls
64 -- @param cafile CA certificates to use for CA-based trust. nil to disable.
65 -- WARNING: as of luasec-1.3.2, CA-based trust checking seems to be thoroughly
66 -- broken and should not be used:
67 -- https://github.com/lunarmodules/luasec/issues/161
69 -- @func check_trust_cb Callback to check for trust of a public key when we've
70 -- previously TOFU-trusted another;
71 -- parameters: fullhost, digest, trusted_times, expiry, cb;
72 -- callback should call cb("always") if the new pubkey should be added to
73 -- the trust store (replacing the old one),
74 -- or cb("once") to proceed with the connection without trusting the pubkey,
75 -- or else cb() to abort connection.
77 -- @func trust_modified_cb Callback called without params when trust_store is
78 -- modified (e.g. so it can be updated on disk)
80 -- @func success_cb Callback called on 20 (success) response;
81 -- parameters: peer_qfile, purl, mimetype, params, cert_info;
82 -- peer_qfile is a quasi-file representing the body,
83 -- supporting :close, :read("l"), :read("a"), and :read(n), with read
84 -- calls returning nil,"aborted" if the connection is aborted via info_cb;
85 -- purl is the final url.parse parsed URI (after any redirects).
87 -- @func error_cb Callback called on connection error or error codes 1,4,5,6;
88 -- parameters: errText, purl, major, minor, meta;
89 -- errText describes the error; the rest are nil except on error code [1456],
90 -- then purl is the final url.parse parsed URI (after any redirects),
91 -- major and minor are the one-digit strings of the error code,
92 -- and meta is any text received in the response.
94 -- @func perm_redir_cb Optional callback called on permanent redirect (31)
95 -- parameters: old_uri, new_uri
97 -- @func info_cb Optional callback to present the progress message
98 -- given as its parameter. Return false to abort the connection.
100 -- @bool confirm_new_tofu Call check_trust_cb (with trusted_times = 0)
101 -- before trusting a key for a new host.
103 -- @string proxy Optional host to proxy request via.
104 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
)
105 info_cb
= info_cb
or function() return true end
107 inner
= function(u
, num_redirects
)
108 local purl
= url
.parse(u
, {scheme
= "gemini", port
= default_port
})
109 if purl
.port
== default_port
then
112 if purl
.scheme
== "gemini" then
116 if not purl
.path
or purl
.path
== "" then
123 return error_cb(T(_("URL too long: %1"), u
))
126 local openssl_options
= {"all", "no_tlsv1", "no_tlsv1_1"}
128 -- Do not allow connecting with a client certificate using TLSv1.2,
129 -- which does not encrypt the certificate.
130 -- (The gemini spec only says that we should warn the user before
131 -- doing so, but that seems not to be possible with luasec.)
132 table.insert(openssl_options
, "no_tlsv1_2")
134 local context
, err
= ssl
.newcontext({
137 options
= openssl_options
,
143 return error_cb(T(_("Error initialising TLS context: %1"), err
))
147 peer
, err
= socket
.tcp()
149 return error_cb(T(_("Error initialising TCP: %1"), err
))
152 local pretty_url
= gemini
.showUrl(u
, true)
153 local function info(stage
, fast
)
154 return info_cb(T("%1\n%2...", pretty_url
, stage
), fast
)
156 if not info("Connecting") then return end
158 local timeout
= 1 -- not too short, because each call to info_cb costs 100ms
159 peer
:settimeout(timeout
)
160 local function with_timeouts(cb
, stage
, errmsg_pat
, fast
, accumulate_partials
)
161 local acc
= accumulate_partials
and ""
163 do_select
= function(e
)
164 if not info(stage
, fast
) then return false end
166 local __
, sock_err
= socket
.select(
167 e
== "wantread" and {peer
} or nil,
168 (e
== "timeout" or e
== "wantwrite") and {peer
} or nil, 1)
169 if sock_err
== "timeout" then
174 local ret
, e
, partial
176 ret
, e
, partial
= cb()
177 if not ret
and partial
and acc
then
180 if e
== "timeout" or e
== "wantread" or e
== "wantwrite" then
181 if not do_select(e
) then
182 return nil, "aborted"
184 elseif e
== "closed" then
187 return error_cb(T(errmsg_pat
, e
))
191 return acc
.. (ret
or "")
199 host
, port
= proxy
:match("^([^:]*):(%d+)")
206 port
= tonumber(purl
.port
or default_port
)
208 if not with_timeouts(function() return peer
:connect(host
, port
) end,
209 _("Connecting"), _("Error connecting to peer: %1"), true)
212 peer
, err
= ssl
.wrap(peer
, context
)
214 return error_cb(T(_("Error on ssl.wrap: %1"), err
))
216 peer
:settimeout(timeout
)
219 if not with_timeouts(function() return peer
:dohandshake() end,
220 _("Handshaking"), _("Error on handshake: %1"))
229 fullhost
= fullhost
.. ":" .. purl
.port
233 local peer_cert
= peer
:getpeercertificate()
234 if not peer_cert
then
235 return error_cb(_("Failed to obtain peer certificate"))
237 local pk_hash
= sha256(peer_cert
:pubkey())
238 local digest
= peer_cert
:digest("sha256")
239 local expiry
= peer_cert
:notafter()
241 local function do_connection(trusted
)
242 if not with_timeouts(function() return peer
:send(u
.."\r\n") end,
243 _("Requesting"), _("Error sending request: %1"))
246 local status
= with_timeouts(function() return peer
:receive("*l") end,
247 _("Receiving header"), _("Error receiving response status : %1"))
248 if not status
then return end
250 local major
, minor
, post
= status
:match("^([1-6])([0-9])(.*)")
251 if not major
or not minor
then
252 return error_cb(T(_("Invalid response status line: %1", status
:sub(1,64))))
254 local meta
= post
:match("^ (.*)") or ""
257 local token_chars
= "[a-zA-Z0-9!#$%%&'*+-.^_`{|}-]" -- from RFC 2045
258 local mimetype
, params_str
= meta
:match("^("..token_chars
.."+/"..token_chars
.."+)(.*)")
259 mimetype
= mimetype
or "text/gemini"
261 while params_str
and params_str
~= "" do
262 local param
, rest
= params_str
:match("^;%s*("..token_chars
.."+=\"[^\"]*\")(.*)")
264 param
, rest
= params_str
:match("^;%s*("..token_chars
.."+="..token_chars
.."+)(.*)")
267 -- ignore unparseable parameters
271 table.insert(params
, param
)
274 local peer_qfile
= {}
275 function peer_qfile
:read(x
)
276 x
= (type(x
) == "string" and x
:match("^[al]$") and "*"..x
)
277 or (type(x
) == "number" and x
)
278 return with_timeouts(function() return peer
:receive(x
) end,
279 _("Receiving body"), _("Error receiving body: %1"), false, true)
281 function peer_qfile
:close()
287 trusted_times
= trusted
and trusted
.trusted_times
or 0,
288 ca
= trusted
and trusted
.ca
290 success_cb(peer_qfile
, purl
, mimetype
, params
, cert_info
)
294 if num_redirects
>= 5 then
295 return error_cb(_("Too many redirects."))
297 local new_uri
= url
.absolute(purl
, meta
)
298 if minor
== "1" and perm_redir_cb
then
299 perm_redir_cb(u
, new_uri
)
301 inner(new_uri
, num_redirects
+ 1)
302 elseif tonumber(major
) < 1 or tonumber(major
) > 6 then
303 error_cb(_("Server returns invalid error code."))
305 local errText
= errTexts
[major
..minor
]
307 errText
= errTexts
[major
.."0"]
309 error_cb(errText
, purl
, major
, minor
, meta
)
314 local function set_trust(times
)
315 trust_store
[fullhost
] = {
320 last_trust_time
= os
.time(),
326 if peer
:getpeerverification() then
328 for _i
, c
in ipairs( peer
:getpeerchain() ) do
331 return do_connection({ ca
= ca_cert
:issuer() })
335 local function trust_cb_cb(user_trust
)
336 if user_trust
== "always" then
338 do_connection(trust_store
[fullhost
])
339 elseif user_trust
== "once" then
346 local trusted
= trust_store
[fullhost
]
348 -- Trust On First Use
349 if confirm_new_tofu
then
350 check_trust_cb(fullhost
, digest
, 0, "", trust_cb_cb
)
352 trust_cb_cb("always")
355 if trusted
.pk_hash
== pk_hash
then
356 local now
= os
.time()
357 if now
- trusted
.last_trust_time
> trusted_times_step
then
358 trusted
.trusted_times
= trusted
.trusted_times
+ 1
359 trusted
.last_trust_time
= now
362 if trusted
.digest
~= digest
then
363 -- Extend trust to update cert with same public key.
364 trusted
.digest
= digest
365 trusted
.expiry
= expiry
368 do_connection(trusted
)
370 check_trust_cb(fullhost
, digest
, trusted
.trusted_times
, trusted
.expiry
, trust_cb_cb
)