1 local Persist
= require("persist")
2 local ButtonDialogTitle
= require("ui/widget/buttondialogtitle")
3 local CheckButton
= require("ui/widget/checkbutton")
4 local MultiInputDialog
= require("ui/widget/multiinputdialog")
5 local ConfirmBox
= require("ui/widget/confirmbox")
6 local InfoMessage
= require("ui/widget/infomessage")
7 local Menu
= require("ui/widget/menu")
8 local Screen
= require("device").screen
9 local UIManager
= require("ui/uimanager")
10 local DataStorage
= require("datastorage")
11 local Version
= require("version")
12 local url
= require("socket.url")
13 local util
= require("util")
14 local _
= require("gettext")
15 local T
= require("ffi/util").template
17 local gemini
= require("gemini")
19 local gemini_dir
= DataStorage
:getDataDir() .. "/gemini"
20 local ids_dir
= gemini_dir
.. "/identities"
23 active_identities_persist
= Persist
:new
{ path
= gemini_dir
.. "/identities.lua" },
26 local function normaliseIdentUrl(u
)
27 local purl
= url
.parse(u
, {scheme
= "gemini", port
= "1965"})
28 if purl
and purl
.scheme
== "titan" then
29 purl
.scheme
= "gemini"
31 if purl
== nil or purl
.scheme
~= "gemini" then
36 -- strip trailing slashes
37 while purl
.path
and purl
.path
:sub(-1) == "/" do
38 purl
.path
= purl
.path
:sub(1,-2)
41 return url
.build(purl
)
44 function Identities
:setup()
45 util
.makePath(ids_dir
)
46 self
.active_identities
= self
.active_identities_persist
:load() or {}
48 self
:renormaliseIdentityUris()
51 function Identities
:renormaliseIdentityUris()
52 local renormalised
= false
53 for u
,__
in pairs(self
.active_identities
) do
54 local n
= normaliseIdentUrl(u
)
56 if not self
.active_identities
[n
] then
57 self
.active_identities
[n
] = self
.active_identities
[u
]
59 self
.active_identities
[u
] = nil
64 self
.active_identities_persist
:save(self
.active_identities
)
68 function Identities
:set(u
, id
)
69 local n
= normaliseIdentUrl(u
)
71 self
.active_identities
[n
] = id
72 self
.active_identities_persist
:save(self
.active_identities
)
76 -- Searches for first identity at or above u.
77 -- Returns identity, url where it was found, and prefix to key and crt paths.
78 function Identities
:get(u
)
79 local n
= normaliseIdentUrl(u
)
84 local id
= self
.active_identities
[n
]
86 return id
, n
, ids_dir
.."/"..id
89 local up
= gemini
.upUrl(u
)
95 local function getIds()
97 util
.findFiles(ids_dir
, function(path
,crt
)
98 if crt
:find("%.crt$") then
99 table.insert(ids
, crt
:sub(0,-5))
106 local function chooseIdentity(callback
)
111 for _i
,id
in ipairs(ids
) do
115 callback
= function()
117 UIManager
:close(widget
)
122 title
= _("Choose identity"),
124 width
= Screen
:getWidth(), -- backwards compatibility;
125 height
= Screen
:getHeight(), -- can delete for KOReader >= 2023.04
127 UIManager
:show(widget
)
130 local function createIdentityInteractive(callback
)
133 local function createId(id
, common_name
, rsa
)
134 local path
= ids_dir
.."/"..id
135 local shell_quoted_name
= common_name
:gsub("'","'\\''")
136 local subj
= shell_quoted_name
== "" and "/" or "/CN="..shell_quoted_name
138 os
.execute("openssl ecparam -genkey -name prime256v1 > "..path
..".key")
139 os
.execute("openssl req -x509 -new -key "..path
..".key -sha256 -out "..path
..".crt -days 2000000 -subj '"..subj
.."'")
141 os
.execute("openssl req -x509 -newkey rsa:2048 -keyout "..path
..".key -sha256 -out "..path
..".crt -days 2000000 -nodes -subj '"..subj
.."'")
143 UIManager
:close(widget
)
146 local function create_cb()
147 local fields
= widget
:getFields()
148 if fields
[1] == "" then
149 UIManager
:show(InfoMessage
:new
{text
= _("Enter a petname for this identity, to be used in this client to refer to the identity.")})
150 elseif not fields
[1]:match("^[%w_%-]+$") then
151 UIManager
:show(InfoMessage
:new
{text
= _("Punctuation not allowed in petname.")})
152 elseif fields
[1]:len() > 12 then
153 UIManager
:show(InfoMessage
:new
{text
= _("Petname too long.")})
154 elseif util
.fileExists(ids_dir
.."/"..fields
[1]..".crt") then
155 UIManager
:show(ConfirmBox
:new
{
156 text
= _("Identity already exists. Overwrite?"),
157 ok_text
= _("Destroy existing identity"),
158 cancel_text
= _("Cancel"),
159 ok_callback
= function()
160 createId(fields
[1], fields
[2], rsa_button
.checked
)
164 createId(fields
[1], fields
[2])
167 widget
= MultiInputDialog
:new
{
168 title
= _("Create identity"),
171 description
= _("Identity petname"),
174 description
= _("Name (optional, sent to server)"),
182 callback
= function()
183 UIManager
:close(widget
)
188 callback
= function()
194 enter_callback
= create_cb
,
196 if Version
:getNormalizedCurrentVersion() < 202408060000 then
197 if widget
.input_fields
then
198 widget
.input_fields
[1].enter_callback
= create_cb
199 widget
.input_fields
[2].enter_callback
= create_cb
200 elseif widget
.input_field
then
201 -- backwards compatibility for <2024.07
202 widget
.input_field
[1].enter_callback
= create_cb
203 widget
.input_field
[2].enter_callback
= create_cb
207 rsa_button
= CheckButton
:new
{
208 text
= _("Use RSA instead of ECDSA"),
212 widget
:addWidget(rsa_button
)
214 UIManager
:show(widget
)
215 widget
:onShowKeyboard()
218 function Identities
:confAt(u
, cb
)
219 local n
= normaliseIdentUrl(u
)
225 local id
= self
:get(n
)
226 local function set_id(new_id
)
230 UIManager
:close(widget
)
239 widget
= ButtonDialogTitle
:new
{
240 title
= T(_("Identity at %1"), gemini
.showUrl(n
)),
244 text
= id
and T(_("Stop using identity %1"), id
) or _("No identity in use"),
246 callback
= function()
249 local c_id
, at
= self
:get(n
)
256 UIManager
:close(widget
)
267 callback
= function()
268 UIManager
:close(widget
)
272 text
= _("Choose identity"),
273 callback
= function()
274 chooseIdentity(set_id
)
278 text
= _("Create identity"),
279 enabled
= os
.execute("openssl version >& /dev/null"),
280 callback
= function()
281 createIdentityInteractive(set_id
)
287 UIManager
:show(widget
)
290 function Identities
:edit()
293 for u
,id
in pairs(self
.active_identities
) do
294 local show_u
= gemini
.showUrl(u
)
295 table.insert(items
, {
296 text
= T("%1: %2", id
, show_u
),
297 callback
= function()
298 UIManager
:show(ConfirmBox
:new
{
299 text
= T(_("Stop using identity %1 at %2?"), id
, show_u
),
301 cancel_text
= _("Cancel"),
302 ok_callback
= function()
303 UIManager
:close(menu
)
311 table.sort(items
, function(i1
,i2
) return i1
.text
< i2
.text
end)
313 title
= _("Active identities"),
315 width
= Screen
:getWidth(), -- backwards compatibility;
316 height
= Screen
:getHeight(), -- can delete for KOReader >= 2023.04