# HG changeset patch # User Kim Alvefur # Date 1315770136 -7200 # Node ID 524dda2ecb6ac4fd6eb30048037b700cd65640d1 # Parent 1082856e4612829e9f61c7ef2d5a6f7488412330 mod_auth_wordpress: Initial commit. diff -r 1082856e4612 -r 524dda2ecb6a mod_auth_wordpress/mod_auth_wordpress.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_auth_wordpress/mod_auth_wordpress.lua Sun Sep 11 21:42:16 2011 +0200 @@ -0,0 +1,252 @@ +-- Wordpress authentication backend for Prosody +-- +-- Copyright (C) 2011 Waqas Hussain +-- + +local log = require "util.logger".init("auth_sql"); +local new_sasl = require "util.sasl".new; +local nodeprep = require "util.encodings".stringprep.nodeprep; +local saslprep = require "util.encodings".stringprep.saslprep; +local DBI = require "DBI" +local md5 = require "util.hashes".md5; +local uuid_gen = require "util.uuid".generate; + +local connection; +local params = 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 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 + 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("`", "\""); + 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 setsql(sql, ...) + local stmt, err = getsql(sql, ...); + if not stmt then return stmt, err; end + return stmt:affected(); +end + +local function get_password(username) + local stmt, err = getsql("SELECT `user_pass` FROM `wp_users` WHERE `user_login`=?", username); + if stmt then + for row in stmt:rows(true) do + return row.user_password; + end + end +end + + +local itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +local function hashEncode64(input, count) + local output = ""; + local i, value = 0, 0; + + while true do + value = input:byte(i+1) + i = i+1; + local idx = value % 0x40 + 1; + output = output .. itoa64:sub(idx, idx); + + if i < count then + value = value + input:byte(i+1) * 256; + end + local _ = value % (2^6); + local idx = ((value - _) / (2^6)) % 0x40 + 1 + output = output .. itoa64:sub(idx, idx); + + if i >= count then break; end + i = i+1; + + if i < count then + value = value + input:byte(i+1) * 256 * 256; + end + local _ = value % (2^12); + local idx = ((value - _) / (2^12)) % 0x40 + 1 + output = output .. itoa64:sub(idx, idx); + + if i >= count then break; end + i = i+1; + + local _ = value % (2^18); + local idx = ((value - _) / (2^18)) % 0x40 + 1 + output = output .. itoa64:sub(idx, idx); + + if not(i < count) then break; end + end + return output; +end +local function hashCryptPrivate(password, genSalt) + local output = "*"; + if not genSalt:match("^%$P%$") then return output; end + + local count_log2 = itoa64:find(genSalt:sub(4,4)) - 1; + if count_log2 < 7 or count_log2 > 30 then return output; end + + local count = 2 ^ count_log2; + local salt = genSalt:sub(5, 12); + + if #salt ~= 8 then return output; end + + local hash = md5(salt..password); + + while true do + hash = md5(hash..password); + if not(count > 1) then break; end + count = count-1; + end + + output = genSalt:sub(1, 12); + output = output .. hashEncode64(hash, 16); + + return output; +end +local function hashGensaltPrivate(input) + local iteration_count_log2 = 6; + local output = "$H$"; + local idx = math.min(iteration_count_log2 + 5, 30) + 1; + output = output .. itoa64:sub(idx, idx); + output = output .. hashEncode64(input, 6); + return output; +end +local function phpbbCheckHash(password, hash) + if #hash == 32 then return hash == md5(password, true); end -- legacy PHPBB2 hash + return #hash == 34 and hashCryptPrivate(password, hash) == hash; +end +local function phpbbCreateHash(password) + local random = uuid_gen():sub(-6); + local salt = hashGensaltPrivate(random); + local hash = hashCryptPrivate(password, salt); + if #hash == 34 then return hash; end + return md5(password, true); +end + + +provider = { name = "wordpress" }; + +function provider.test_password(username, password) + local hash = get_password(username); + return hash and phpbbCheckHash(password, hash); +end +function provider.user_exists(username) + module:log("debug", "test user %s existence", username); + return get_password(username) and true; +end + +function provider.get_password(username) + return nil, "Getting password is not supported."; +end +function provider.set_password(username, password) + local hash = phpbbCreateHash(password); + local stmt, err = setsql("UPDATE `wp_users` SET `user_pass`=? WHERE `user_login`=?", hash, username); + return stmt and true, err; +end +function provider.create_user(username, password) + return nil, "Account creation/modification not supported."; +end + +local escapes = { + [" "] = "\\20"; + ['"'] = "\\22"; + ["&"] = "\\26"; + ["'"] = "\\27"; + ["/"] = "\\2f"; + [":"] = "\\3a"; + ["<"] = "\\3c"; + [">"] = "\\3e"; + ["@"] = "\\40"; + ["\\"] = "\\5c"; +}; +local unescapes = {}; +for k,v in pairs(escapes) do unescapes[v] = k; end +local function jid_escape(s) return s and (s:gsub(".", escapes)); end +local function jid_unescape(s) return s and (s:gsub("\\%x%x", unescapes)); end + +function provider.get_sasl_handler() + local sasl = {}; + function sasl:clean_clone() return provider.get_sasl_handler(); end + function sasl:mechanisms() return { PLAIN = true; }; end + function sasl:select(mechanism) + if not self.selected and mechanism == "PLAIN" then + self.selected = mechanism; + return true; + end + end + function sasl:process(message) + if not message then return "failure", "malformed-request"; end + local authorization, authentication, password = message:match("^([^%z]*)%z([^%z]+)%z([^%z]+)"); + if not authorization then return "failure", "malformed-request"; end + authentication = saslprep(authentication); + password = saslprep(password); + if (not password) or (password == "") or (not authentication) or (authentication == "") then + return "failure", "malformed-request", "Invalid username or password."; + end + local function test(authentication) + local prepped = nodeprep(authentication); + local normalized = jid_unescape(prepped); + return normalized and provider.test_password(normalized, password) and prepped; + end + local username = test(authentication) or test(jid_escape(authentication)); + if username then + self.username = username; + return "success"; + end + return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent."; + end + return sasl; +end + +module:add_item("auth-provider", provider); +