Skip to content

Instantly share code, notes, and snippets.

@jhass
Last active March 15, 2018 13:14
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save jhass/948e8e8d87b9143f97ad to your computer and use it in GitHub Desktop.
Save jhass/948e8e8d87b9143f97ad to your computer and use it in GitHub Desktop.
Prosody authentication for Diaspora. Tested with Prosody 0.9.4. See https://wiki.diasporafoundation.org/Integration/XMPP/Prosody
-- Based on Simple SQL Authentication module for Prosody IM
-- Copyright (C) 2011 Tomasz Sterna <tomek@xiaoka.com>
-- Copyright (C) 2011 Waqas Hussain <waqas20@gmail.com>
--
-- 25/05/2014: Modified for Diaspora by Anahuac de Paula Gil - anahuac@anahuac.eu
-- 06/08/2014: Cleaned up and fixed SASL auth by Jonne Haß <me@jhass.eu>
-- 22/11/2014: Allow token authentication by Jonne Haß <me@jhass.eu>
local log = require "util.logger".init("auth_diaspora")
local new_sasl = require "util.sasl".new
local DBI = require "DBI"
local bcrypt = require "bcrypt"
local connection
local params = module:get_option("auth_diaspora", module:get_option("auth_sql", module:get_option("sql")))
local resolve_relative_path = require "core.configmanager".resolve_relative_path
local function test_connection()
if not connection then return nil; end
if connection:ping() then
return true
else
module:log("debug", "Database connection closed")
connection = nil
end
end
local function set_encoding(conn)
if params.driver ~= "MySQL" then return; end
local set_names_query = "SET NAMES '%s';"
local stmt = assert(conn:prepare("SET NAMES 'utf8mb4';"));
assert(stmt:execute());
end
local function connect()
if not test_connection() then
prosody.unlock_globals()
local dbh, err = DBI.Connect(
params.driver, params.database,
params.username, params.password,
params.host, params.port
)
prosody.lock_globals()
if not dbh then
module:log("debug", "Database connection failed: %s", tostring(err))
return nil, err
end
set_encoding(dbh);
module:log("debug", "Successfully connected to database");
dbh:autocommit(true); -- don't run in transaction
connection = dbh
return connection
end
end
do -- process options to get a db connection
params = params or { driver = "SQLite3" }
if params.driver == "SQLite3" then
params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite")
end
assert(params.driver and params.database, "Both the SQL driver and the database need to be specified")
assert(connect())
end
local function getsql(sql, ...)
if params.driver == "PostgreSQL" then
sql = sql:gsub("`", "\"")
elseif params.driver == "MySQL" then
sql = sql:gsub(";$", " CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';")
end
if not test_connection() then connect(); end
-- do prepared statement stuff
local stmt, err = connection:prepare(sql)
if not stmt and not test_connection() then error("connection failed"); end
if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
-- run query
local ok, err = stmt:execute(...)
if not ok and not test_connection() then error("connection failed"); end
if not ok then return nil, err; end
return stmt
end
local function get_password(username)
local stmt, err = getsql("SELECT encrypted_password FROM users WHERE locked_at IS NULL AND username = ?", username)
if stmt then
for row in stmt:rows(true) do
return row.encrypted_password
end
end
end
local function get_token(username)
local stmt, err = getsql("SELECT authentication_token FROM users WHERE locked_at IS NULL AND username = ?", username)
if stmt then
for row in stmt:rows(true) do
return row.authentication_token
end
end
end
local function test_password(username, password)
-- pepper imported from diaspora/config/initializers/devise.rb
local pepper = "065eb8798b181ff0ea2c5c16aee0ff8b70e04e2ee6bd6e08b49da46924223e39127d5335e466207d42bf2a045c12be5f90e92012a4f05f7fc6d9f3c875f4c95b"
-- adding pepper to the regular password
local pw_plus_pepper = password .. pepper
-- Getting password from Diaspora database
local pw_stored = get_password(username)
-- Comparing password. If fail aborts
return password and pw_stored and bcrypt.verify(pw_plus_pepper, pw_stored)
end
local function test_token(username, token)
local stored_token = get_token(username)
return stored_token and token == stored_token
end
provider = {};
function provider.test_password(username, password)
return test_password(username, password) or test_token(username, password)
end
function provider.get_password(username)
return get_password(username)
end
function provider.set_password(username, password)
return nil, "Setting password is not supported."
end
function provider.user_exists(username)
return get_password(username) and true
end
function provider.create_user(username, password)
return nil, "Account creation/modification not supported."
end
function provider.get_sasl_handler()
local profile = {
plain_test = function(sasl, username, password, realm)
return provider.test_password(username, password), true
end
}
return new_sasl(module.host, profile)
end
function provider.users()
local stmt, err = getsql("SELECT username FROM users WHERE locked_at IS NULL AND username != ''")
if stmt then
local next, state = stmt:rows(true)
return function()
for row in next, state do
return row.username
end
end
end
return stmt, err
end
module:provides("auth", provider)
-- Prosody module to import diaspora contacts into a users roster.
-- Inspired by mod_auth_sql and mod_groups of the Prosody software.
--
-- As with mod_groups the change is not permanent and thus any changes
-- to the imported contacts will be lost.
--
-- The MIT License (MIT)
--
-- Copyright (c) <2014> <Jonne Haß <me@jhass.eu>>
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.
local log = require "util.logger".init("diaspora_contacts")
local DBI = require "DBI"
local jid, datamanager = require "util.jid", require "util.datamanager"
local jid_prep = jid.prep
local rostermanager = require "core.rostermanager"
local module_host = module:get_host()
local host = prosody.hosts[module_host]
local connection
local params = module:get_option("diaspora_contacts", module:get_option("auth_diaspora", module:get_option("auth_sql", module:get_option("sql"))))
local function test_connection()
if not connection then return nil; end
if connection:ping() then
return true
else
module:log("debug", "Database connection closed")
connection = nil
end
end
local function set_encoding(conn)
if params.driver ~= "MySQL" then return; end
local set_names_query = "SET NAMES '%s';"
local stmt = assert(conn:prepare("SET NAMES 'utf8mb4';"));
assert(stmt:execute());
end
local function connect()
if not test_connection() then
prosody.unlock_globals()
local dbh, err = DBI.Connect(
params.driver, params.database,
params.username, params.password,
params.host, params.port
)
prosody.lock_globals()
if not dbh then
module:log("debug", "Database connection failed: %s", tostring(err))
return nil, err
end
set_encoding(dbh);
module:log("debug", "Successfully connected to database")
dbh:autocommit(true) -- don't run in transaction
connection = dbh
return connection
end
end
do -- process options to get a db connection
assert(params.driver and params.database, "Both the SQL driver and the database need to be specified")
assert(connect())
end
local function getsql(sql, ...)
if params.driver == "PostgreSQL" then
sql = sql:gsub("`", "\"")
elseif params.driver == "MySQL" then
sql = sql:gsub(";$", " CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';")
end
if not test_connection() then connect(); end
-- do prepared statement stuff
local stmt, err = connection:prepare(sql)
if not stmt and not test_connection() then error("connection failed"); end
if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
-- run query
local ok, err = stmt:execute(...)
if not ok and not test_connection() then error("connection failed"); end
if not ok then return nil, err; end
return stmt;
end
local function get_contacts(username)
module:log("debug", "loading contacts for %s", username)
local contacts = {}
local stmt, err = getsql([[
SELECT people.diaspora_handle AS jid,
COALESCE(NULLIF(CONCAT(first_name, ' ', last_name), ' '), people.diaspora_handle) AS name,
CONCAT(aspects.name, ' (Diaspora)') AS group_name,
CASE
WHEN sharing = true AND receiving = true THEN 'both'
WHEN sharing = true AND receiving = false THEN 'to'
WHEN sharing = false AND receiving = true THEN 'from'
ELSE 'none'
END AS subscription
FROM contacts
JOIN people ON people.id = contacts.person_id
JOIN profiles ON profiles.person_id = people.id
JOIN users ON users.id = contacts.user_id
JOIN aspect_memberships ON aspect_memberships.contact_id = contacts.id
JOIN aspects ON aspects.id = aspect_memberships.aspect_id
WHERE (receiving = true OR sharing = true)
AND chat_enabled = true
AND username = ?
]], username)
if stmt then
for row in stmt:rows(true) do
if not contacts[row.jid] then
contacts[row.jid] = {}
contacts[row.jid].subscription = row.subscription
contacts[row.jid].name = row.name
contacts[row.jid].groups = {}
end
contacts[row.jid].groups[row.group_name] = true
end
return contacts
end
end
local function update_roster(roster, contacts, update_action)
if not contacts then return; end
for user_jid, contact in pairs(contacts) do
local updated = false
if not roster[user_jid] then
roster[user_jid] = {}
roster[user_jid].subscription = contact.subscription
roster[user_jid].name = contact.name
roster[user_jid].persist = false
updated = true
end
if not roster[user_jid].groups then
roster[user_jid].groups = {}
end
for group in pairs(contact.groups) do
if not roster[user_jid].groups[group] then
roster[user_jid].groups[group] = true
updated = true
end
end
for group in pairs(roster[user_jid].groups) do
if not contact.groups[group] then
roster[user_jid].groups[group] = nil
updated = true
end
end
if updated and update_action then
update_action(user_jid)
end
end
for user_jid, contact in pairs(roster) do
if contact.persist == false then
if not contacts[user_jid] then
roster[user_jid] = nil
if update_action then
update_action(user_jid)
end
end
end
end
end
function bump_roster_version(roster)
if roster[false] then
roster[false].version = (tonumber(roster[false].version) or 0) + 1
end
end
local function update_roster_contacts(username, host, roster)
update_roster(roster, get_contacts(username), function (user_jid)
module:log("debug", "pushing roster update to %s for %s", jid.join(username, host), user_jid)
bump_roster_version(roster)
rostermanager.roster_push(username, host, user_jid)
end)
end
function inject_roster_contacts(event, var2, var3)
local username = ""
local host = ""
local roster = {}
if type(event) == "table" then
module:log("debug", "Prosody 0.10 or trunk detected. Use event variable.")
username = event.username
host = event.host
roster = event.roster
else
module:log("debug", "Prosody 0.9.x detected, Use old variable style.")
username = event
host = var2
roster = var3
end
local fulljid = jid.join(username, host)
module:log("debug", "injecting contacts for %s", fulljid)
update_roster(roster, get_contacts(username))
bump_roster_version(roster)
end
function update_all_rosters()
module:log("debug", "updating all rosters")
for username, user in pairs(host.sessions) do
module:log("debug", "Updating roster for %s", jid.join(username, module_host))
update_roster_contacts(username, module_host, rostermanager.load_roster(username, module_host))
end
return 300
end
function remove_virtual_contacts(username, host, datastore, roster)
if host == module_host and datastore == "roster" then
module:log("debug", "removing injected contacts before storing roster of %s", jid.join(username, host))
local new_roster = {}
for jid, contact in pairs(roster) do
if contact.persist ~= false then
new_roster[jid] = contact
end
end
if roster[false] then
new_roster[false] = {}
new_roster[false].version = roster[false].version
end
return username, host, datastore, new_roster
end
return username, host, datastore, roster
end
function module.load()
module:hook("roster-load", inject_roster_contacts)
module:add_timer(300, update_all_rosters)
datamanager.add_callback(remove_virtual_contacts)
end
function module.unload()
datamanager.remove_callback(remove_virtual_contacts)
end
@asdofindia
Copy link

data in line 263 is undefined, isn't it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment