Mercurial > prosody-modules
annotate mod_invites_api/mod_invites_api.lua @ 5193:2bb29ece216b
mod_http_oauth2: Implement stateless dynamic client registration
Replaces previous explicit registration that required either the
additional module mod_adhoc_oauth2_client or manually editing the
database. That method was enough to have something to test with, but
would not probably not scale easily.
Dynamic client registration allows creating clients on the fly, which
may be even easier in theory.
In order to not allow basically unauthenticated writes to the database,
we implement a stateless model here.
per_host_key := HMAC(config -> oauth2_registration_key, hostname)
client_id := JWT { client metadata } signed with per_host_key
client_secret := HMAC(per_host_key, client_id)
This should ensure everything we need to know is part of the client_id,
allowing redirects etc to be validated, and the client_secret can be
validated with only the client_id and the per_host_key.
A nonce injected into the client_id JWT should ensure nobody can submit
the same client metadata and retrieve the same client_secret
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Fri, 03 Mar 2023 21:14:19 +0100 |
parents | 1cae382e88a1 |
children |
rev | line source |
---|---|
4115
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
1 local http_formdecode = require "net.http".formdecode; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
2 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
3 local api_key_store; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
4 local invites; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
5 -- COMPAT: workaround to avoid executing inside prosodyctl |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
6 if prosody.shutdown then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
7 module:depends("http"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
8 api_key_store = module:open_store("invite_api_keys", "map"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
9 invites = module:depends("invites"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
10 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
11 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
12 local function get_api_user(request, params) |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
13 local combined_key; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
14 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
15 local auth_header = request.headers.authorization; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
16 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
17 if not auth_header then |
5143
1cae382e88a1
mod_invites_api: Fix traceback when no query params (thanks Menel)
Matthew Wild <mwild1@gmail.com>
parents:
5142
diff
changeset
|
18 params = params or http_formdecode(request.url.query or "="); |
4115
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
19 combined_key = params.key; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
20 else |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
21 local auth_type, value = auth_header:match("^(%S+)%s(%S+)$"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
22 if auth_type ~= "Bearer" then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
23 return; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
24 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
25 combined_key = value; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
26 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
27 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
28 if not combined_key then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
29 return; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
30 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
31 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
32 local key_id, key_token = combined_key:match("^([^/]+)/(.+)$"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
33 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
34 if not key_id then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
35 return; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
36 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
37 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
38 local api_user = api_key_store:get(nil, key_id); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
39 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
40 if not api_user or api_user.token ~= key_token then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
41 return; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
42 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
43 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
44 -- TODO: key expiry, rate limiting, etc. |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
45 return api_user; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
46 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
47 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
48 function handle_request(event) |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
49 local query_params = http_formdecode(event.request.url.query); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
50 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
51 local api_user = get_api_user(event.request, query_params); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
52 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
53 if not api_user then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
54 return 403; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
55 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
56 |
4216
35b678609b79
mod_invites_api: Allow restricting HTTP methods per key (once implemented)
Matthew Wild <mwild1@gmail.com>
parents:
4115
diff
changeset
|
57 if api_user.allowed_methods and not api_user.allowed_methods[event.request.method] then |
35b678609b79
mod_invites_api: Allow restricting HTTP methods per key (once implemented)
Matthew Wild <mwild1@gmail.com>
parents:
4115
diff
changeset
|
58 return 405; |
35b678609b79
mod_invites_api: Allow restricting HTTP methods per key (once implemented)
Matthew Wild <mwild1@gmail.com>
parents:
4115
diff
changeset
|
59 end |
35b678609b79
mod_invites_api: Allow restricting HTTP methods per key (once implemented)
Matthew Wild <mwild1@gmail.com>
parents:
4115
diff
changeset
|
60 |
4115
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
61 local invite = invites.create_account(nil, { source = "api/token/"..api_user.id }); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
62 if not invite then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
63 return 500; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
64 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
65 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
66 event.response.headers.Location = invite.landing_page or invite.uri; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
67 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
68 if query_params.redirect then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
69 return 303; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
70 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
71 return 201; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
72 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
73 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
74 if invites then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
75 module:provides("http", { |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
76 route = { |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
77 ["GET"] = handle_request; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
78 }; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
79 }); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
80 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
81 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
82 function module.command(arg) |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
83 if #arg < 2 then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
84 print("Usage:"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
85 print(""); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
86 print(" prosodyctl mod_"..module.name.." create NAME"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
87 print(" prosodyctl mod_"..module.name.." delete KEY_ID"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
88 print(" prosodyctl mod_"..module.name.." list"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
89 print(""); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
90 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
91 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
92 local command = table.remove(arg, 1); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
93 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
94 local host = table.remove(arg, 1); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
95 if not prosody.hosts[host] then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
96 print("Error: please supply a valid host"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
97 return 1; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
98 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
99 require "core.storagemanager".initialize_host(host); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
100 module.host = host; --luacheck: ignore 122/module |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
101 api_key_store = module:open_store("invite_api_keys", "map"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
102 |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
103 if command == "create" then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
104 local id = require "util.id".short(); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
105 local token = require "util.id".long(); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
106 api_key_store:set(nil, id, { |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
107 id = id; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
108 token = token; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
109 name = arg[1]; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
110 created_at = os.time(); |
4216
35b678609b79
mod_invites_api: Allow restricting HTTP methods per key (once implemented)
Matthew Wild <mwild1@gmail.com>
parents:
4115
diff
changeset
|
111 allowed_methods = { GET = true, POST = true }; |
4115
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
112 }); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
113 print(id.."/"..token); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
114 elseif command == "delete" then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
115 local id = table.remove(arg, 1); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
116 if not api_key_store:get(nil, id) then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
117 print("Error: key not found"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
118 return 1; |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
119 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
120 api_key_store:set(nil, id, nil); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
121 elseif command == "list" then |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
122 local api_key_store_kv = module:open_store("invite_api_keys"); |
5142
410d7c8d210d
mod_invites_api: Fix traceback on list command with no entries (thanks mirux)
Matthew Wild <mwild1@gmail.com>
parents:
4216
diff
changeset
|
123 for key_id, key_info in pairs(api_key_store_kv:get(nil) or {}) do |
4115
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
124 print(key_id, key_info.name or "<unknown>"); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
125 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
126 else |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
127 print("Unknown command - "..command); |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
128 end |
165ade4ce97b
mod_invites_api: New module to create new invites over HTTP
Matthew Wild <mwild1@gmail.com>
parents:
diff
changeset
|
129 end |