prosody: Broad update to upstream (prosody-modules) where appropriate

This commit is contained in:
Matthew Wild 2021-01-27 13:11:36 +00:00
parent 2207199a60
commit 1bff00eba7
18 changed files with 23 additions and 1300 deletions

View File

@ -84,14 +84,25 @@ modules_enabled = {
"welcome"; -- Welcome users who register accounts
"http_files"; -- Serve static files from a directory over HTTP
"reload_modules";
"landing_page";
-- Invites
"groups_migration";
"invites";
"invites_adhoc";
"invites_api";
"invites_groups";
"invites_page";
"invites_register";
"invites_register_api";
"invites_tracking";
"invites_api";
"easy_invite";
"watchregistrations";
"firewall";
-- Circles
"groups_migration";
-- For the web portal
"http_oauth2";
"http_admin_api";
@ -113,7 +124,7 @@ legacy_ssl_ports = { 5223 }
allow_registration = true
registration_invite_only = true
invites_page = ENV_SNIKKET_INVITE_URL or ("https://"..DOMAIN.."/_/invite?{token}");
invites_page = ENV_SNIKKET_INVITE_URL or ("https://"..DOMAIN.."/invite?{token}");
c2s_require_encryption = true
s2s_require_encryption = true

View File

@ -78,7 +78,6 @@
- mod_cloud_notify_encrypted
- mod_cloud_notify_priority_tag
- mod_cloud_notify_filters
- mod_invite
- mod_block_registrations
- mod_compact_resource
- mod_conversejs
@ -86,7 +85,6 @@
- mod_lastlog
- mod_limit_auth
- mod_password_policy
- mod_password_reset
- mod_roster_allinall
- mod_strict_https
- mod_vcard_muc
@ -95,21 +93,21 @@
- mod_http_altconnect
- mod_bookmarks
- mod_default_bookmarks
- mod_muc_defaults
- mod_muc_local_only
- mod_firewall
- mod_turncredentials
- mod_admin_notify
- mod_http_oauth2
- mod_http_admin_api
- mod_rest
- name: Install Bootstrap and JS libs
apt:
name:
- libjs-bootstrap4
- libjs-jquery
install_recommends: no
- mod_groups_migration
- mod_invites
- mod_invites_adhoc
- mod_invites_api
- mod_invites_groups
- mod_invites_page
- mod_invites_register
- mod_invites_register_api
- mod_invites_tracking
- name: Enable wanted modules
file:
@ -117,12 +115,6 @@
src: "/usr/local/lib/snikket-modules/{{item}}"
dest: "/etc/prosody/modules/{{item}}"
loop:
- mod_landing_page
- mod_invites
- mod_invites_page
- mod_invites_register
- mod_invites_api
- mod_easy_invite
- mod_update_check
- mod_update_notify
- mod_authz_internal

View File

@ -1,244 +0,0 @@
-- XEP-0401: Easy User Onboarding
local dataforms = require "util.dataforms";
local datetime = require "util.datetime";
local jid_bare = require "util.jid".bare;
local jid_split = require "util.jid".split;
local split_jid = require "util.jid".split;
local rostermanager = require "core.rostermanager";
local st = require "util.stanza";
local invite_only = module:get_option_boolean("registration_invite_only", true);
local require_encryption = module:get_option_boolean("c2s_require_encryption",
module:get_option_boolean("require_encryption", false));
local new_adhoc = module:require("adhoc").new;
-- Whether local users can invite other users to create an account on this server
local allow_user_invites = module:get_option_boolean("allow_user_invites", true);
local invites;
if prosody.shutdown then -- COMPAT hack to detect prosodyctl
invites = module:depends("invites");
end
local invite_result_form = dataforms.new({
title = "Your Invite",
-- TODO instructions = something helpful
{
name = "uri";
label = "Invite URI";
-- TODO desc = something helpful
},
{
name = "url" ;
var = "landing-url";
label = "Invite landing URL";
},
{
name = "expire";
label = "Token valid until";
},
});
module:depends("adhoc");
module:provides("adhoc", new_adhoc("New Invite", "urn:xmpp:invite#invite",
function (_, data)
local username = split_jid(data.from);
local invite = invites.create_contact(username, allow_user_invites);
--TODO: check errors
return {
status = "completed";
form = {
layout = invite_result_form;
values = {
uri = invite.uri;
url = invite.landing_page;
expire = datetime.datetime(invite.expires);
};
};
};
end, "local_user"));
-- TODO
-- module:provides("adhoc", new_adhoc("Create account", "urn:xmpp:invite#create-account", function () end, "admin"));
-- XEP-0379: Pre-Authenticated Roster Subscription
module:hook("presence/bare", function (event)
local stanza = event.stanza;
if stanza.attr.type ~= "subscribe" then return end
local preauth = stanza:get_child("preauth", "urn:xmpp:pars:0");
if not preauth then return end
local token = preauth.attr.token;
if not token then return end
local username, host = jid_split(stanza.attr.to);
local invite, err = invites.get(token, username);
if not invite then
module:log("debug", "Got invalid token, error: %s", err);
return;
end
local contact = jid_bare(stanza.attr.from);
module:log("debug", "Approving inbound subscription to %s from %s", username, contact);
if rostermanager.set_contact_pending_in(username, host, contact, stanza) then
if rostermanager.subscribed(username, host, contact) then
invite:use();
rostermanager.roster_push(username, host, contact);
-- Send back a subscription request (goal is mutual subscription)
if not rostermanager.is_user_subscribed(username, host, contact)
and not rostermanager.is_contact_pending_out(username, host, contact) then
module:log("debug", "Sending automatic subscription request to %s from %s", contact, username);
if rostermanager.set_contact_pending_out(username, host, contact) then
rostermanager.roster_push(username, host, contact);
module:send(st.presence({type = "subscribe", to = contact }));
else
module:log("warn", "Failed to set contact pending out for %s", username);
end
end
end
end
end, 1);
-- TODO sender side, magic automatic mutual subscription
local invite_stream_feature = st.stanza("register", { xmlns = "urn:xmpp:invite" }):up();
module:hook("stream-features", function(event)
local session, features = event.origin, event.features;
-- Advertise to unauthorized clients only.
if session.type ~= "c2s_unauthed" or (require_encryption and not session.secure) then
return
end
features:add_child(invite_stream_feature);
end);
-- Client is submitting a preauth token to allow registration
module:hook("stanza/iq/urn:xmpp:pars:0:preauth", function(event)
local preauth = event.stanza.tags[1];
local token = preauth.attr.token;
local validated_invite = invites.get(token);
if not validated_invite then
local reply = st.error_reply(event.stanza, "cancel", "forbidden", "The invite token is invalid or expired");
event.origin.send(reply);
return true;
end
event.origin.validated_invite = validated_invite;
local reply = st.reply(event.stanza);
event.origin.send(reply);
return true;
end);
-- Registration attempt - ensure a valid preauth token has been supplied
module:hook("user-registering", function (event)
local validated_invite = event.validated_invite or (event.session and event.session.validated_invite);
if invite_only and not validated_invite then
event.allowed = false;
event.reason = "Registration on this server is through invitation only";
return;
end
if validated_invite.additional_data and validated_invite.additional_data.allow_reset then
event.allow_reset = validated_invite.additional_data.allow_reset;
end
end);
-- Make a *one-way* subscription. User will see when contact is online,
-- contact will not see when user is online.
function subscribe(host, user_username, contact_username)
local user_jid = user_username.."@"..host;
local contact_jid = contact_username.."@"..host;
-- Update user's roster to say subscription request is pending...
rostermanager.set_contact_pending_out(user_username, host, contact_jid);
-- Update contact's roster to say subscription request is pending...
rostermanager.set_contact_pending_in(contact_username, host, user_jid);
-- Update contact's roster to say subscription request approved...
rostermanager.subscribed(contact_username, host, user_jid);
-- Update user's roster to say subscription request approved...
rostermanager.process_inbound_subscription_approval(user_username, host, contact_jid);
end
-- Make a mutual subscription between jid1 and jid2. Each JID will see
-- when the other one is online.
function subscribe_both(host, user1, user2)
subscribe(host, user1, user2);
subscribe(host, user2, user1);
end
-- Registration successful, if there was a preauth token, mark it as used
module:hook("user-registered", function (event)
local validated_invite = event.validated_invite or (event.session and event.session.validated_invite);
if not validated_invite then
return;
end
local inviter_username = validated_invite.inviter;
local contact_username = event.username;
validated_invite:use();
if inviter_username then
module:log("debug", "Creating mutual subscription between %s and %s", inviter_username, contact_username);
subscribe_both(module.host, inviter_username, contact_username);
end
if validated_invite.additional_data then
module:log("debug", "Importing roles from invite");
local roles = validated_invite.additional_data.roles;
if roles then
module:open_store("roles"):set(contact_username, roles);
end
end
end);
-- Equivalent of user-registered but for when the account already existed
-- (i.e. password reset)
module:hook("user-password-reset", function (event)
local validated_invite = event.validated_invite or (event.session and event.session.validated_invite);
if not validated_invite then
return;
end
validated_invite:use();
end);
local sm = require "core.storagemanager";
function module.command(arg)
if #arg < 2 or arg[2] ~= "generate" then
print("usage: prosodyctl mod_easy_invite example.net generate");
return;
end
local host = arg[1];
assert(hosts[host], "Host "..tostring(host).." does not exist");
sm.initialize_host(host);
-- Load mod_invites
invites = module:context(host):depends("invites");
module:context(host):depends("invites_page");
table.remove(arg, 1);
table.remove(arg, 1);
local invite, roles;
if arg[1] == "--reset" then
local nodeprep = require "util.encodings".stringprep.nodeprep;
local username = nodeprep(arg[2]);
if not username then
print("Please supply a valid username to generate a reset link for");
return;
end
invite = invites.create_account_reset(username);
else
if arg[1] == "--admin" then
roles = { ["prosody:admin"] = true };
elseif arg[1] == "--role" then
roles = { [arg[2]] = true };
end
invite = invites.create_account(nil, { roles = roles });
end
print(invite.landing_page or invite.uri);
end

View File

@ -1,12 +0,0 @@
local serve = require "net.http.files".serve;
module:set_global();
local path = module:get_option_string("acme_challenge_path", "/var/www/.well-known/acme-challenge");
module:provides("http", {
default_path = "/.well-known/acme-challenge";
route = {
["GET /*"] = serve({ path = path });
}
});

View File

@ -1,13 +0,0 @@
local mime_map = module:shared("/*/http_files/mime").types or {
css = "text/css",
js = "application/javascript",
};
local libjs_path = module:get_option_string("libjs_path", "/usr/share/javascript");
module:provides("http", {
default_path = "/share";
route = {
["GET /*"] = require "net.http.files".serve({ path = libjs_path, mime_map = mime_map });
}
});

View File

@ -1,140 +0,0 @@
local id = require "util.id";
local url = require "socket.url";
local jid_node = require "util.jid".node;
local invite_ttl = module:get_option_number("invite_expiry", 86400 * 7);
local token_storage = module:open_store("invite_token", "map");
local function get_uri(action, jid, token, params) --> string
return url.build({
scheme = "xmpp",
path = jid,
query = action..";preauth="..token..(params and (";"..params) or ""),
});
end
local function create_invite(invite_action, invite_jid, allow_registration, additional_data)
local token = id.medium();
local created_at = os.time();
local expires = created_at + invite_ttl;
local invite_params = (invite_action == "roster" and allow_registration) and "ibr=y" or nil;
local invite = {
type = invite_action;
jid = invite_jid;
token = token;
allow_registration = allow_registration;
additional_data = additional_data;
uri = get_uri(invite_action, invite_jid, token, invite_params);
created_at = created_at;
expires = expires;
};
module:fire_event("invite-created", invite);
if allow_registration then
local ok, err = token_storage:set(nil, token, invite);
if not ok then
module:log("warn", "Failed to store account invite: %s", err);
return nil, "internal-server-error";
end
end
if invite_action == "roster" then
local username = jid_node(invite_jid);
local ok, err = token_storage:set(username, token, expires);
if not ok then
module:log("warn", "Failed to store subscription invite: %s", err);
return nil, "internal-server-error";
end
end
return invite;
end
-- Create invitation to register an account (optionally restricted to the specified username)
function create_account(account_username, additional_data) --luacheck: ignore 131/create_account
local jid = account_username and (account_username.."@"..module.host) or module.host;
return create_invite("register", jid, true, additional_data);
end
-- Create invitation to reset the password for an account
function create_account_reset(account_username) --luacheck: ignore 131/create_account_reset
return create_account(account_username, { allow_reset = account_username });
end
-- Create invitation to become a contact of a local user
function create_contact(username, allow_registration, additional_data) --luacheck: ignore 131/create_contact
return create_invite("roster", username.."@"..module.host, allow_registration, additional_data);
end
local valid_invite_methods = {};
local valid_invite_mt = { __index = valid_invite_methods };
function valid_invite_methods:use()
if self.username then
-- Also remove the contact invite if present, on the
-- assumption that they now have a mutual subscription
token_storage:set(self.username, self.token, nil);
end
token_storage:set(nil, self.token, nil);
return true;
end
-- Get a validated invite (or nil, err). Must call :use() on the
-- returned invite after it is actually successfully used
-- For "roster" invites, the username of the local user (who issued
-- the invite) must be passed.
-- If no username is passed, but the registration is a roster invite
-- from a local user, the "inviter" field of the returned invite will
-- be set to their username.
function get(token, username)
if not token then
return nil, "no-token";
end
local valid_until, inviter;
-- Fetch from host store (account invite)
local token_info = token_storage:get(nil, token);
if username then -- token being used for subscription
-- Fetch from user store (subscription invite)
valid_until = token_storage:get(username, token);
else -- token being used for account creation
valid_until = token_info and token_info.expires;
if token_info and token_info.type == "roster" then
username = jid_node(token_info.jid);
inviter = username;
end
end
if not valid_until then
module:log("debug", "Got unknown token: %s", token);
return nil, "token-invalid";
elseif os.time() > valid_until then
module:log("debug", "Got expired token: %s", token);
return nil, "token-expired";
end
return setmetatable({
token = token;
username = username;
inviter = inviter;
type = token_info and token_info.type or "roster";
uri = token_info and token_info.uri or get_uri("roster", username.."@"..module.host, token);
additional_data = token_info and token_info.additional_data or nil;
}, valid_invite_mt);
end
function use(token) --luacheck: ignore 131/use
local invite = get(token);
return invite and invite:use();
end

View File

@ -1,112 +0,0 @@
local http_formdecode = require "net.http".formdecode;
local api_key_store;
local invites;
-- COMPAT: workaround to avoid executing inside prosodyctl
if prosody.shutdown then
module:depends("http");
api_key_store = module:open_store("invite_api_keys", "map");
invites = module:depends("invites");
end
local function get_api_user(request, params)
local combined_key;
local auth_header = request.headers.authorization;
if not auth_header then
params = params or http_formdecode(request.url.query);
combined_key = params.key;
else
local auth_type, value = auth_header:match("^(%S+)%s(%S+)$");
if auth_type ~= "Bearer" then
return;
end
combined_key = value;
end
if not combined_key then
return;
end
local key_id, key_token = combined_key:match("^([^/]+)/(.+)$");
if not key_id then
return;
end
local api_user = api_key_store:get(nil, key_id);
if not api_user or api_user.token ~= key_token then
return;
end
-- TODO: key expiry, rate limiting, etc.
return api_user;
end
function handle_request(event)
local query_params = http_formdecode(event.request.url.query);
local api_user = get_api_user(event.request, query_params);
if not api_user then
return 403;
end
local invite = invites.create_account(nil, { source = "api/token/"..api_user.id });
if not invite then
return 500;
end
event.response.headers.Location = invite.landing_page or invite.uri;
if query_params.redirect then
return 303;
end
return 201;
end
if invites then
module:provides("http", {
route = {
["GET"] = handle_request;
};
});
end
function module.command(arg)
local host = table.remove(arg, 1);
if not prosody.hosts[host] then
print("Error: please supply a valid host");
return 1;
end
require "core.storagemanager".initialize_host(host);
module.host = host; --luacheck: ignore 122/module
api_key_store = module:open_store("invite_api_keys", "map");
local command = table.remove(arg, 1);
if command == "create" then
local id = require "util.id".short();
local token = require "util.id".long();
api_key_store:set(nil, id, {
id = id;
token = token;
name = arg[1];
created_at = os.time();
});
print(id.."/"..token);
elseif command == "delete" then
local id = table.remove(arg, 1);
if not api_key_store:get(nil, id) then
print("Error: key not found");
return 1;
end
api_key_store:set(nil, id, nil);
elseif command == "list" then
local api_key_store_kv = module:open_store("invite_api_keys");
for key_id, key_info in pairs(api_key_store_kv:get(nil)) do
print(key_id, key_info.name or "<unknown>");
end
end
end

View File

@ -1,131 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invite to {site_name} | Snikket</title>
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/snikket.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#fbd308">
<meta name="theme-color" content="#fbd308">
<style>
#install-buttons-container {
text-align: center;
}
#install-buttons-container img {
height: 3.5em;
margin: 0 auto 0.5em auto;
}
button {
text-align: center;
}
</style>
</head>
<body>
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
<div class="card rounded-lg shadow">
<h1 class="card-header rounded-lg rounded-lg">
Invite to {site_name}<br/>
</h1>
<div id="powered-by">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></div>
<div class="card-body" >
{inviter?<p>You have been invited to chat on {site_name} using Snikket,
a secure, privacy-friendly chat app.</p>}
{inviter&<p>You have been invited to chat with {inviter} using Snikket,
a secure, privacy-friendly chat app on {site_name}.</p>}
<h5 class="card-title">Get started</h5>
<p>Install the Snikket app on your Android device (iOS <a href="https://snikket.org/faq/#is-there-an-ios-app">coming soon!</a>)</p>
<div id="install-buttons-container" class="container">
<a href='https://play.google.com/store/apps/details?id=org.snikket.android&referrer={uri|urlescape}&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'>
<img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png'/>
</a>
<a href="#qr-modal" class="d-none" id="qr-modal-show">
<button class="btn btn-info" title="Send this invite to your device"
data-toggle="modal" data-target="#qr-modal">Not on mobile?</button>
</a>
</div>
<p>After installation the app should automatically open and prompt you to
create an account. If not, simply click the button below.</p>
<h6 class="text-center">App already installed?</h6>
<div class="text-center">
<a href="{uri}" id="uri-cta"><button class="btn btn-secondary btn-sm">Open the app</button></a><br/>
<small class="text-muted">This button works only if you have the app installed already!</small>
</div>
<br/>
<h5>Alternatives</h5>
<p>You can connect to Snikket using any XMPP-compatible software. If the button above does not
work with your app, you may need to <a href="register?{token}">register an account manually</a>.</p>
</div>
</div>
</div>
<div class="modal" tabindex="-1" role="dialog" id="qr-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Scan invite code</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>You can transfer this invite to your mobile device by scanning a code with your camera. You can use
either a QR scanner app or the Snikket app itself.</p>
<nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<a class="nav-item nav-link active" id="qr-tab-scanner" data-toggle="tab" href="#qr-info-url" role="tab" aria-controls="qr-info-url" aria-selected="true">Using a QR code scanner</a>
<a class="nav-item nav-link" id="qr-tab-app" data-toggle="tab" href="#qr-info-uri" role="tab" aria-controls="qr-info-uri" aria-selected="false">Using the Snikket app</a>
</div>
</nav>
<div class="tab-content">
<div id="qr-info-url" class="tab-pane show active">
<p>Use a <em>QR code</em> scanner on your mobile device to scan the code below:</p>
<div id="qr-invite-page" class="w-50 p-1 mx-auto"></div>
</div>
<div id="qr-info-uri" class="tab-pane">
<div>
<img src="/img/snikket-scan-button-shdw.png" class="d-block w-25 p-1 float-right">
<p>Install the Snikket app on your mobile device, open it, and
tap the 'Scan' button at the top.</p>
<p>Your camera will turn on. Point it at the square code below until it is
within the highlighted square on your screen, and wait until the app
recognises it.</p>
</div>
<div id="qr-uri" class="w-50 p-1 mx-auto clearfix"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script src="/share/jquery/jquery.min.js"></script>
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
<script src="/js/qrcode.min.js"></script>
<script type="text/javascript">
$('#qr-modal').one('show.bs.modal', function (e) {
new QRCode(document.getElementById("qr-uri"), document.getElementById("uri-cta").getAttribute("href"));
new QRCode(document.getElementById("qr-invite-page"), document.location.href);
});
$('#qr-modal-show').addClass("d-md-block");
</script>
</body>
</html>

View File

@ -1,37 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invite to {site_name} | Snikket</title>
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/snikket.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#fbd308">
<meta name="theme-color" content="#fbd308">
</head>
<body>
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
<div class="card rounded-lg shadow">
<h1 class="card-header rounded-lg rounded-lg">
Invite to {site_name}<br/>
</h1>
<div id="powered-by">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></div>
<div class="card-body" >
<h5 class="card-title">Invite expired</h5>
<p>Sorry, it looks like this invite code has expired!</p>
<img class="w-100" src="/img/illus-empty.svg">
</div>
</div>
</div>
<script src="/share/jquery/jquery.min.js"></script>
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
</body>
</html>

View File

@ -1,55 +0,0 @@
local st = require "util.stanza";
local url_escape = require "util.http".urlencode;
local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape, {
urlescape = url_escape;
});
local render_url = require "util.interpolation".new("%b{}", url_escape, {
urlescape = url_escape;
noscheme = function (url)
return (url:gsub("^[^:]+:", ""));
end;
});
local site_name = module:get_option_string("site_name", module.host);
if prosody.shutdown then
module:depends("http");
end
local invites = module:depends("invites");
-- Point at eg https://github.com/ge0rg/easy-xmpp-invitation
local base_url = module:get_option_string("invites_page", (module.http_url and module:http_url().."?{token}") or nil);
local function add_landing_url(invite)
if not base_url then return; end
invite.landing_page = render_url(base_url, invite);
end
module:hook("invite-created", add_landing_url);
function serve_invite_page(event)
local invite_page_template = assert(module:load_resource("html/invite.html")):read("*a");
local invalid_invite_page_template = assert(module:load_resource("html/invite_invalid.html")):read("*a");
local invite = invites.get(event.request.url.query);
if not invite then
return render_html_template(invalid_invite_page_template, { site_name = site_name });
end
local invite_page = render_html_template(invite_page_template, {
site_name = site_name;
token = invite.token;
uri = invite.uri;
type = invite.type;
jid = invite.jid;
});
return invite_page;
end
module:provides("http", {
route = {
["GET"] = serve_invite_page;
};
});

View File

@ -1,87 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{site_name} | Snikket</title>
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/snikket.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#fbd308">
<meta name="theme-color" content="#fbd308">
</head>
<body>
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
<div class="card rounded-lg shadow">
<h1 class="card-header rounded-lg rounded-lg">
Secure communication on {site_name}<br/>
</h1>
<div id="powered-by">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></div>
<div class="card-body" >
<p>{site_name} is using Snikket - a secure, privacy-friendly chat app.</p>
<h5 class="card-title">Create an account</h5>
<p>Creating an account will allow to communicate with other people using
the Snikket app or compatible software. If you already have the app installed,
we recommend that you continue the account creation process inside the app
by clicking on the button below:</p>
<h6 class="text-center">App already installed?</h6>
<div class="text-center">
<a href="{uri}"><button class="btn btn-secondary btn-sm">Open the app</button></a><br/>
<small class="text-muted">This button works only if you have the app installed already!</small>
</div>
<br/>
<h5 class="card-title">Create an account online</h5>
<p>If you plan to use a legacy XMPP client, you can register an account online and enter your
credentials into any XMPP-compatible software.</p>
{message&<div class="alert {msg_class?alert-info}" role="alert">
{message}
</div>}
<form method="post">
<div class="form-group form-row">
<label for="user" class="col-md-4 col-lg-12 col-form-label">Username:</label>
<div class="col-md-8 col-lg-12">
<div class="input-group">
<input
type="text" name="user" class="form-control" aria-describedby="usernameHelp"
required autofocus minlength="1" maxlength="30" length="30"
>
<div class="input-group-append">
<span class="input-group-text">@{domain}</span>
</div>
</div>
<small id="usernameHelp" class="d-block form-text text-muted">Choose a username, this will become the first part of your new chat address.</small>
</div>
</div>
<div class="form-group form-row">
<label for="password" class="col-md-4 col-lg-12 col-form-label">Password:</label>
<div class="col-md-8 col-lg-12">
<input type="password" name="password" class="form-control" aria-describedby="passwordHelp"
autocomplete="new-password"
>
<small id="passwordHelp" class="form-text text-muted">Enter a secure password that you do not use anywhere else.</small>
</div>
</div>
<div class="form-group form-row">
<input type="hidden" name="token" value="{token}">
<button type="submit" class="btn btn-primary btn-lg">Submit</button>
</div>
</form>
</div>
</div>
</div>
<script src="/share/jquery/jquery.min.js"></script>
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
</body>
</html>

View File

@ -1,37 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invite to {site_name} | Snikket</title>
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/snikket.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#fbd308">
<meta name="theme-color" content="#fbd308">
</head>
<body>
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
<div class="card rounded-lg shadow">
<h1 class="card-header rounded-lg rounded-lg">
Invite to {site_name}<br/>
</h1>
<div id="powered-by">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></div>
<div class="card-body" >
<h5 class="card-title">Registration error</h5>
<p>{message?Sorry, there was a problem registering your account.}</p>
<img class="w-100" src="/img/illus-bug.svg">
</div>
</div>
</div>
<script src="/share/jquery/jquery.min.js"></script>
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
</body>
</html>

View File

@ -1,79 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invite to {site_name} | Snikket</title>
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/snikket.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#fbd308">
<meta name="theme-color" content="#fbd308">
<script>
function toggle_password(e) {
var button = e.target;
var input = button.parentNode.parentNode.querySelector("input");
switch(input.attributes.type.value) {
case "password":
input.attributes.type.value = "text";
button.innerText = "Hide";
break;
case "text":
input.attributes.type.value = "password";
button.innerText = "Show";
break;
}
}
</script>
</head>
<body>
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
<div class="card rounded-lg shadow">
<h1 class="card-header rounded-lg rounded-lg">
{site_name}<br/>
</h1>
<div id="powered-by">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></div>
<div class="card-body" >
<h5 class="card-title">Congratulations!</h5>
<p>You have created an account on {site_name}.</p>
<p>To start chatting, you need to enter your new account
credentials into your chosen XMPP software.</p>
<p>As a final reminder, your account details are shown below:</p>
<form class="account-details col-12 col-lg-6 mx-auto">
<div class="form-group form-row">
<label for="user" class="col-md-4 col-lg-12 col-form-label">Chat address (JID):</label>
<div class="col-md-8 col-lg-12">
<input type="text" class="form-control-plaintext" readonly value="{username}@{domain}">
</div>
</div>
<div class="form-group form-row">
<label for="password" class="col-md-4 col-lg-12 col-form-label">Password:</label>
<div class="col-md-8 col-lg-12">
<div class="input-group">
<input type="password" readonly class="form-control" value="{password}">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" onclick="toggle_password(event)">Show</button>
</div>
</div>
</div>
</div>
</form>
<p>Your password is stored encrypted on the server and will not be accessible after you close this page.</p>
</div>
</div>
</div>
<script src="/share/jquery/jquery.min.js"></script>
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
</body>
</html>

View File

@ -1,158 +0,0 @@
local id = require "util.id";
local http_formdecode = require "net.http".formdecode;
local usermanager = require "core.usermanager";
local nodeprep = require "util.encodings".stringprep.nodeprep;
local st = require "util.stanza";
local url_escape = require "util.http".urlencode;
local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape, {
urlescape = url_escape;
});
local site_name = module:get_option_string("site_name", module.host);
module:depends("http");
module:depends("easy_invite");
local invites = module:depends("invites");
local invites_page = module:depends("invites_page");
function serve_register_page(event)
local register_page_template = assert(module:load_resource("html/register.html")):read("*a");
local invite = invites.get(event.request.url.query);
if not invite then
return {
status_code = 303;
headers = {
["Location"] = invites.module:http_url().."?"..event.request.url.query;
};
};
end
local invite_page = render_html_template(register_page_template, {
site_name = site_name;
token = invite.token;
domain = module.host;
uri = invite.uri;
type = invite.type;
jid = invite.jid;
});
return invite_page;
end
function handle_register_form(event)
local request, response = event.request, event.response;
local form_data = http_formdecode(request.body);
local user, password, token = form_data["user"], form_data["password"], form_data["token"];
local register_page_template = assert(module:load_resource("html/register.html")):read("*a");
local error_template = assert(module:load_resource("html/register_error.html")):read("*a");
local success_template = assert(module:load_resource("html/register_success.html")):read("*a");
local invite = invites.get(token);
if not invite then
return {
status_code = 303;
headers = {
["Location"] = invites_page.module:http_url().."?"..event.request.url.query;
};
};
end
response.headers.content_type = "text/html; charset=utf-8";
if not user or #user == 0 or not password or #password == 0 or not token then
return render_html_template(register_page_template, {
site_name = site_name;
token = invite.token;
domain = module.host;
uri = invite.uri;
type = invite.type;
jid = invite.jid;
msg_class = "alert-warning";
message = "Please fill in all fields.";
});
end
-- Shamelessly copied from mod_register_web.
local prepped_username = nodeprep(user);
if not prepped_username or #prepped_username == 0 then
return render_html_template(register_page_template, {
site_name = site_name;
token = invite.token;
domain = module.host;
uri = invite.uri;
type = invite.type;
jid = invite.jid;
msg_class = "alert-warning";
message = "This username contains invalid characters.";
});
end
if usermanager.user_exists(prepped_username, module.host) then
return render_html_template(register_page_template, {
site_name = site_name;
token = invite.token;
domain = module.host;
uri = invite.uri;
type = invite.type;
jid = invite.jid;
msg_class = "alert-warning";
message = "This username is already in use.";
});
end
local registering = {
validated_invite = invite;
username = prepped_username;
host = module.host;
allowed = true;
};
module:fire_event("user-registering", registering);
if not registering.allowed then
return render_html_template(error_template, {
site_name = site_name;
msg_class = "alert-danger";
message = registering.reason or "Registration is not allowed.";
});
end
local ok, err = usermanager.create_user(prepped_username, password, module.host);
if ok then
module:fire_event("user-registered", {
username = prepped_username;
host = module.host;
source = "mod_"..module.name;
validated_invite = invite;
});
return render_html_template(success_template, {
site_name = site_name;
username = prepped_username;
domain = module.host;
password = password;
});
else
local err_id = id.short();
module:log("warn", "Registration failed (%s): %s", err_id, tostring(err));
return render_html_template(error_template, {
site_name = site_name;
msg_class = "alert-danger";
message = ("An unknown error has occurred (%s)"):format(err_id);
});
end
end
module:provides("http", {
route = {
["GET"] = serve_register_page;
["POST"] = handle_register_form;
};
});

View File

@ -1,11 +0,0 @@
<html>
<body>
TODO!
Hello from <strong>{site_name}!</strong>
You have been invited to join {site_name}. Simply click here to get started: <a href="{invite_page}">view invite</a>
If you already have a compatible app installed, you can click here instead: <a href="{invite_uri}">open in app</a>
</body>
</html>

View File

@ -1,7 +0,0 @@
TODO!
Hello from {site_name}!
You have been invited to join {site_name}. Simply click here to get started: {invite_page}
If you already have a compatible app installed, you can click here instead: {invite_uri}

View File

@ -1,63 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{site_name} | Snikket</title>
<link rel="stylesheet" href="/share/bootstrap4/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/snikket.css">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#fbd308">
<meta name="theme-color" content="#fbd308">
</head>
<body>
<div id="background" class="fixed-top overflow-hidden" aria-role="none presentation"></div>
<div id="form" class="container col-md-8 col-md-offset-2 col-sm-8 cold-sm-offset-2 col-lg-6 col-lg-offset-3 mt-2 mt-md-5">
<div class="card rounded-lg shadow">
<h1 class="card-header rounded-lg rounded-lg">
Secure communication on {site_name}<br/>
</h1>
<div id="powered-by"><a href="https://snikket.org/">Powered by <img src="/img/snikket-logo-text.svg" alt="Snikket"></a></div>
<div class="card-body" >
<p>{site_name} is using Snikket - a secure, privacy-friendly chat app.</p>
{allow_email&
<h5 class="card-title">Request invitation</h5>
<p>You may join the Snikket network by creating an account
on {site_name}. Registration is by invitation only, enter
your email address to request an invitation.</p>
<form action="/_/invite-request" method="post">
<div class="form-group form-row">
<label for="user" class="col-md-4 col-lg-12 col-form-label">Email:</label>
<div class="col-md-8 col-lg-12">
<div class="input-group">
<input
type="email" name="email" class="form-control" aria-describedby="emailHelp"
required autofocus minlength="3" length="30"
>
</div>
<small id="emailHelp" class="d-block form-text text-muted">
Enter the email address we should deliver your invitation to.
<br/>
Your email address will be kept private as per our <a href="privacy/">Privacy Policy</a>.
</small>
</div>
</div>
<div class="form-group form-row float-right">
<button type="submit" class="btn btn-primary btn-lg">Submit</button>
</div>
</form>
}
</div>
</div>
</div>
<script src="/share/jquery/jquery.min.js"></script>
<script src="/share/bootstrap4/js/bootstrap.min.js"></script>
</body>
</html>

View File

@ -1,94 +0,0 @@
local http_formdecode = require "net.http".formdecode;
local st = require "util.stanza";
local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape);
local render_text_template = require"util.interpolation".new("%b{}", function (s) return s; end);
local mime = require "mime";
local ltn12 = require "ltn12";
local site_name = module:get_option_string("site_name", module.host);
-- Email templates
local email_template_preamble = "Problems viewing this email? View it online at {invite_page}";
local email_template_text = assert(module:load_resource("email_templates/invite_email.txt")):read("*a");
local email_template_html = assert(module:load_resource("email_templates/invite_email.html")):read("*a");
module:depends("http");
module:depends("email");
local invites = module:depends("invites");
local landing_page_template = assert(module:load_resource("html/index.html")):read("*a");
local landing_page = render_html_template(landing_page_template, {
site_name = site_name;
});
local function handle_form(event)
local request, response = event.request, event.response;
local form_data = http_formdecode(request.body);
local email = form_data["email"];
response.headers.content_type = "";
local invite = invites.create_account();
local email_template_params = {
site_name = site_name;
invite_token = invite.token;
invite_uri = invite.uri;
invite_page = invite.landing_page;
};
local email_headers = {
Subject = "Your Snikket invitation";
["Content-Type"] = "multipart/mixed";
};
local email_body = {
-- Optional text content prefixed to the entire email (visible even in
-- non-MIME clients)
preamble = render_text_template(email_template_preamble, email_template_params);
-- Plain text version
[1] = {
headers = {
["Content-Type"] = 'text/plain; charset="utf-8"';
};
body = mime.eol(0, render_text_template(email_template_text, email_template_params));
};
-- HTML version
[2] = {
headers = {
["content-type"] = 'text/html;charset="utf-8"',
["content-transfer-encoding"] = "quoted-printable"
},
body = ltn12.source.chain(
ltn12.source.string(render_html_template(email_template_html, email_template_params)),
ltn12.filter.chain(
mime.encode("quoted-printable", "text"),
mime.wrap()
)
);
};
}
module:send_email({ --luacheck: ignore 143/module
to = email;
headers = email_headers;
body = email_body;
});
return render_html_template(landing_page_template, {
site_name = site_name;
message = "Ok! Check your inbox :)";
});
end
module:provides("http", {
route = {
["GET /"] = landing_page;
["POST /invite-request"] = handle_form;
};
});