Software /
code /
prosody-modules
Comparison
mod_http_xep227/mod_http_xep227.lua @ 4865:bd0a1f917d98
mod_http_xep227: New module providing HTTP API for account data import/export
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Wed, 12 Jan 2022 16:42:08 +0000 |
child | 4867:9d29467f4d5b |
comparison
equal
deleted
inserted
replaced
4864:62006f4022e9 | 4865:bd0a1f917d98 |
---|---|
1 local it = require "util.iterators"; | |
2 local http = require "util.http"; | |
3 local sm = require "core.storagemanager"; | |
4 local xml = require "util.xml"; | |
5 | |
6 local tokens = module:depends("tokenauth"); | |
7 module:depends("storage_xep0227"); | |
8 | |
9 local archive_store_name = module:get_option("archive_store", "archive"); | |
10 | |
11 local known_stores = { | |
12 accounts = "keyval"; | |
13 roster = "keyval"; | |
14 private = "keyval"; | |
15 pep = "keyval"; | |
16 vcard = "keyval"; | |
17 | |
18 [archive_store_name] = "archive"; | |
19 pep_data = "archive"; | |
20 }; | |
21 | |
22 local function new_user_xml(username, host) | |
23 local user_xml; | |
24 | |
25 return { | |
26 set_user_xml = function (_, store_username, store_host, new_xml) | |
27 if username ~= store_username or store_host ~= host then | |
28 return nil; | |
29 end | |
30 user_xml = new_xml; | |
31 return true; | |
32 end; | |
33 | |
34 get_user_xml = function (_, store_username, store_host) | |
35 if username ~= store_username or store_host ~= host then | |
36 return nil; | |
37 end | |
38 return user_xml; | |
39 end | |
40 }; | |
41 end | |
42 | |
43 local function get_selected_stores(query_params) | |
44 local selected_kv_stores, selected_archive_stores, export_pep_data = {}, {}, false; | |
45 if query_params.stores then | |
46 for store_name in query_params.stores:gmatch("[^,]+") do | |
47 local store_type = known_stores[store_name]; | |
48 if store_type == "keyval" then | |
49 table.insert(selected_kv_stores, store_name); | |
50 elseif store_type == "archive" then | |
51 if store_name == "pep_data" then | |
52 export_pep_data = true; | |
53 else | |
54 table.insert(selected_archive_stores, store_name); | |
55 end | |
56 else | |
57 module:log("warn", "Unknown store: %s", store_name); | |
58 return 400; | |
59 end | |
60 end | |
61 end | |
62 return { | |
63 keyval = selected_kv_stores; | |
64 archive = selected_archive_stores; | |
65 export_pep_data = export_pep_data; | |
66 }; | |
67 end | |
68 | |
69 local function get_config_driver(store_name, host) | |
70 -- Fiddling to handle the 'pep_data' storage config override | |
71 if store_name:find("pep_", 1, true) == 1 then | |
72 store_name = "pep_data"; | |
73 end | |
74 -- Return driver | |
75 return sm.get_driver(session.host, driver_store_name); | |
76 end | |
77 | |
78 local function handle_export_227(event) | |
79 local session = assert(event.session, "No session found"); | |
80 local xep227_driver = sm.load_driver(session.host, "xep0227"); | |
81 | |
82 local username = session.username; | |
83 | |
84 local user_xml = new_user_xml(session.username, session.host); | |
85 | |
86 local query_params = http.formdecode(event.request.url.query or ""); | |
87 | |
88 local selected_stores = get_selected_stores(query_params); | |
89 | |
90 for store_name in it.values(selected_stores.keyval) do | |
91 -- Open the source store that contains the data | |
92 local store = sm.open(session.host, store_name); | |
93 -- Read the current data | |
94 local data, err = store:get(username); | |
95 if data ~= nil or not err then | |
96 -- Initialize the destination store (XEP-0227 backed) | |
97 local target_store = xep227_driver:open_xep0227(store_name, nil, user_xml); | |
98 -- Transform the data and update user_xml (via the _set_user_xml callback) | |
99 if not target_store:set(username, data == nil and {} or data) then | |
100 return 500; | |
101 end | |
102 elseif err then | |
103 return 500; | |
104 end | |
105 end | |
106 | |
107 if selected_stores.export_pep_data then | |
108 local pep_node_list = sm.open(session.host, "pep"):get(session.username); | |
109 if pep_node_list then | |
110 for node_name in it.keys(pep_node_list) do | |
111 table.insert(selected_stores.archive, "pep_"..node_name); | |
112 end | |
113 end | |
114 end | |
115 | |
116 for store_name in it.values(selected_stores.archive) do | |
117 local source_driver = get_config_driver(store_name, session.host); | |
118 local source_archive = source_driver:open(store_name, "archive"); | |
119 local dest_archive = xep227_driver:open_xep0227(store_name, "archive", user_xml); | |
120 local count, errs = 0, 0; | |
121 for id, item, when, with in source_archive:find(username) do | |
122 local ok, err = dest_archive:append(username, id, item, when, with); | |
123 if ok then | |
124 count = count + 1; | |
125 else | |
126 module:log("warn", "Error: %s", err); | |
127 errs = errs + 1; | |
128 end | |
129 if ( count + errs ) % 100 == 0 then | |
130 module:log("info", "%d items migrated, %d errors", count, errs); | |
131 end | |
132 end | |
133 end | |
134 | |
135 if not user_xml or not user_xml:find("host/user") then | |
136 module:log("warn", "No data to export: %s", tostring(user_xml)); | |
137 return 204; | |
138 end | |
139 | |
140 event.response.headers["Content-Type"] = "application/xml"; | |
141 return [[<?xml version="1.0" encoding="utf-8" ?>]]..tostring(user_xml); | |
142 end | |
143 | |
144 local function is_looking_like_xep227(xml_data) | |
145 if not xml_data or xml_data.name ~= "server-data" | |
146 or xml_data.attr.xmlns ~= "urn:xmpp:pie:0" then | |
147 return false; | |
148 end | |
149 -- Looks like 227, but check it has at least one host + user element | |
150 return not not input_xml_parsed:find("host/user"); | |
151 end | |
152 | |
153 local function handle_import_227(event) | |
154 local session = assert(event.session, "No session found"); | |
155 local username = session.username; | |
156 | |
157 local input_xml_raw = event.request.body; | |
158 local input_xml_parsed = xml.parse(input_xml_raw); | |
159 | |
160 -- Some sanity checks | |
161 if not input_xml_parsed or not is_looking_like_227(input_xml_parsed) then | |
162 module:log("warn", "No data to import"); | |
163 return 422; | |
164 end | |
165 | |
166 -- Set the host and username of the import to the new account's user/host | |
167 input_xml_parsed:find("host").attr.jid = session.host; | |
168 input_xml_parsed:find("host/user").attr.name = username; | |
169 | |
170 local user_xml = new_user_xml(session.username, session.host); | |
171 | |
172 user_xml:set_user_xml(username, session.host, input_xml_parsed); | |
173 | |
174 local xep227_driver = sm.load_driver(session.host, "xep0227"); | |
175 | |
176 local selected_stores = get_selected_stores(event.request.url.query); | |
177 | |
178 for _, store_name in ipairs(selected_stores.keyval) do | |
179 -- Initialize the destination store (XEP-0227 backed) | |
180 local store = xep227_driver:open_xep0227(store_name, nil, user_xml); | |
181 | |
182 -- Read the current data | |
183 local data, err = store:get(username); | |
184 if data ~= nil or not err then | |
185 local target_store = sm.open(session.host, store_name); | |
186 -- Transform the data and update user_xml (via the _set_user_xml callback) | |
187 if not target_store:set(username, data == nil and {} or data) then | |
188 return 500; | |
189 end | |
190 elseif err then | |
191 return 500; | |
192 end | |
193 end | |
194 | |
195 if selected_stores.export_pep_data then | |
196 local pep_store = xep227_driver:open_xep0277("pep", nil, user_xml); | |
197 local pep_node_list = pep_store:get(session.username); | |
198 if pep_node_list then | |
199 for node_name in it.keys(pep_node_list) do | |
200 table.insert(selected_stores.archive, "pep_"..node_name); | |
201 end | |
202 end | |
203 end | |
204 | |
205 for store_name in it.values(selected_stores.archive) do | |
206 local source_archive = xep227_driver:open_xep0227(store_name, "archive", user_xml); | |
207 local dest_driver = get_config_driver(store_name, session.host); | |
208 local dest_archive = dest_driver:open(store_name, "archive"); | |
209 local count, errs = 0, 0; | |
210 for id, item, when, with in source_archive:find(username) do | |
211 local ok, err = dest_archive:append(username, id, item, when, with); | |
212 if ok then | |
213 count = count + 1; | |
214 else | |
215 module:log("warn", "Error: %s", err); | |
216 errs = errs + 1; | |
217 end | |
218 if ( count + errs ) % 100 == 0 then | |
219 module:log("info", "%d items migrated, %d errors", count, errs); | |
220 end | |
221 end | |
222 end | |
223 | |
224 return 200; | |
225 end | |
226 | |
227 --- | |
228 | |
229 local function check_credentials(request) | |
230 local auth_type, auth_data = string.match(request.headers.authorization or "", "^(%S+)%s(.+)$"); | |
231 if not (auth_type and auth_data) then | |
232 return false; | |
233 end | |
234 | |
235 if auth_type == "Bearer" then | |
236 local token_info = tokens.get_token_info(auth_data); | |
237 if not token_info or not token_info.session then | |
238 return false; | |
239 end | |
240 return token_info.session; | |
241 end | |
242 return nil; | |
243 end | |
244 | |
245 local function check_auth(routes) | |
246 local function check_request_auth(event) | |
247 local session = check_credentials(event.request); | |
248 if not session then | |
249 event.response.headers.authorization = ("Bearer realm=%q"):format(module.host.."/"..module.name); | |
250 return false, 401; | |
251 elseif session.auth_scope ~= "prosody:scope:admin" then | |
252 return false, 403; | |
253 end | |
254 event.session = session; | |
255 return true; | |
256 end | |
257 | |
258 for route, handler in pairs(routes) do | |
259 routes[route] = function (event, ...) | |
260 local permit, code = check_request_auth(event); | |
261 if not permit then | |
262 return code; | |
263 end | |
264 return handler(event, ...); | |
265 end; | |
266 end | |
267 return routes; | |
268 end | |
269 | |
270 module:provides("http", { | |
271 route = check_auth { | |
272 ["GET /export"] = handle_export_227; | |
273 ["PUT /import"] = handle_import_227; | |
274 }; | |
275 }); |