Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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

This comment has been minimized.

Copy link

@asdofindia asdofindia commented Sep 28, 2015

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
You can’t perform that action at this time.