index.gmi: give repo.or.cz url
[gemini.koplugin.git] / identities.lua
blobabe871fe6ed2b6fdea909de3ee0fca48893c09d8
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"
22 local 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"
30 end
31 if purl == nil or purl.scheme ~= "gemini" then
32 return nil
33 end
34 purl.query = nil
36 -- strip trailing slashes
37 while purl.path and purl.path:sub(-1) == "/" do
38 purl.path = purl.path:sub(1,-2)
39 end
41 return url.build(purl)
42 end
44 function Identities:setup()
45 util.makePath(ids_dir)
46 self.active_identities = self.active_identities_persist:load() or {}
48 self:renormaliseIdentityUris()
49 end
51 function Identities:renormaliseIdentityUris()
52 local renormalised = false
53 for u,__ in pairs(self.active_identities) do
54 local n = normaliseIdentUrl(u)
55 if n ~= u then
56 if not self.active_identities[n] then
57 self.active_identities[n] = self.active_identities[u]
58 end
59 self.active_identities[u] = nil
60 renormalised = true
61 end
62 end
63 if renormalised then
64 self.active_identities_persist:save(self.active_identities)
65 end
66 end
68 function Identities:set(u, id)
69 local n = normaliseIdentUrl(u)
70 if n then
71 self.active_identities[n] = id
72 self.active_identities_persist:save(self.active_identities)
73 end
74 end
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)
80 if n == nil then
81 return nil
82 end
84 local id = self.active_identities[n]
85 if id then
86 return id, n, ids_dir.."/"..id
87 end
89 local up = gemini.upUrl(u)
90 if up ~= u then
91 return self:get(up)
92 end
93 end
95 local function getIds()
96 local ids = {}
97 util.findFiles(ids_dir, function(path,crt)
98 if crt:find("%.crt$") then
99 table.insert(ids, crt:sub(0,-5))
101 end)
102 table.sort(ids)
103 return ids
106 local function chooseIdentity(callback)
107 local ids = getIds()
109 local widget
110 local items = {}
111 for _i,id in ipairs(ids) do
112 table.insert(items,
114 text = id,
115 callback = function()
116 callback(id)
117 UIManager:close(widget)
118 end,
121 widget = Menu:new{
122 title = _("Choose identity"),
123 item_table = items,
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)
131 local widget
132 local rsa_button
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
137 if not rsa then
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.."'")
140 else
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)
144 callback(id)
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)
161 end,
163 else
164 createId(fields[1], fields[2])
167 widget = MultiInputDialog:new{
168 title = _("Create identity"),
169 fields = {
171 description = _("Identity petname"),
174 description = _("Name (optional, sent to server)"),
177 buttons = {
180 text = _("Cancel"),
181 id = "close",
182 callback = function()
183 UIManager:close(widget)
184 end,
187 text = _("Create"),
188 callback = function()
189 create_cb()
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"),
209 checked = false,
210 parent = widget,
212 widget:addWidget(rsa_button)
214 UIManager:show(widget)
215 widget:onShowKeyboard()
218 function Identities:confAt(u, cb)
219 local n = normaliseIdentUrl(u)
220 if n == nil then
221 return
224 local widget
225 local id = self:get(n)
226 local function set_id(new_id)
227 if new_id then
228 self:set(n, new_id)
229 id = new_id
230 UIManager:close(widget)
231 if cb then
232 cb(id)
233 else
234 self:confAt(n)
239 widget = ButtonDialogTitle:new{
240 title = T(_("Identity at %1"), gemini.showUrl(n)),
241 buttons = {
244 text = id and T(_("Stop using identity %1"), id) or _("No identity in use"),
245 enabled = id ~= nil,
246 callback = function()
247 local delId
248 delId = function()
249 local c_id, at = self:get(n)
250 if c_id then
251 self:set(at, nil)
252 delId()
255 delId()
256 UIManager:close(widget)
257 if cb then
258 cb(nil)
260 end,
265 text = _("Cancel"),
266 id = "close",
267 callback = function()
268 UIManager:close(widget)
272 text = _("Choose identity"),
273 callback = function()
274 chooseIdentity(set_id)
275 end,
278 text = _("Create identity"),
279 enabled = os.execute("openssl version >& /dev/null"),
280 callback = function()
281 createIdentityInteractive(set_id)
282 end,
287 UIManager:show(widget)
290 function Identities:edit()
291 local menu
292 local items = {}
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),
300 ok_text = _("Stop"),
301 cancel_text = _("Cancel"),
302 ok_callback = function()
303 UIManager:close(menu)
304 self:set(u, nil)
305 self:edit()
306 end,
308 end,
311 table.sort(items, function(i1,i2) return i1.text < i2.text end)
312 menu = Menu:new{
313 title = _("Active identities"),
314 item_table = items,
315 width = Screen:getWidth(), -- backwards compatibility;
316 height = Screen:getHeight(), -- can delete for KOReader >= 2023.04
318 UIManager:show(menu)
321 return Identities