mod_muc_webchat_url: Fix default url
[prosody-modules.git] / mod_auth_phpbb3 / mod_auth_phpbb3.lua
blob54a6b7cb3dd0e2614e4364d80e2cf2672e0d4442
1 -- phpbb3 authentication backend for Prosody
2 --
3 -- Copyright (C) 2011 Waqas Hussain
4 --
6 local log = require "util.logger".init("auth_sql");
7 local new_sasl = require "util.sasl".new;
8 local nodeprep = require "util.encodings".stringprep.nodeprep;
9 local saslprep = require "util.encodings".stringprep.saslprep;
10 local DBI = require "DBI"
11 local md5 = require "util.hashes".md5;
12 local uuid_gen = require "util.uuid".generate;
13 local have_bcrypt, bcrypt = pcall(require, "bcrypt"); -- available from luarocks
15 local connection;
16 local params = module:get_option("sql");
18 local resolve_relative_path = require "core.configmanager".resolve_relative_path;
20 local function test_connection()
21 if not connection then return nil; end
22 if connection:ping() then
23 return true;
24 else
25 module:log("debug", "Database connection closed");
26 connection = nil;
27 end
28 end
29 local function connect()
30 if not test_connection() then
31 prosody.unlock_globals();
32 local dbh, err = DBI.Connect(
33 params.driver, params.database,
34 params.username, params.password,
35 params.host, params.port
37 prosody.lock_globals();
38 if not dbh then
39 module:log("debug", "Database connection failed: %s", tostring(err));
40 return nil, err;
41 end
42 module:log("debug", "Successfully connected to database");
43 dbh:autocommit(true); -- don't run in transaction
44 connection = dbh;
45 return connection;
46 end
47 end
49 do -- process options to get a db connection
50 params = params or { driver = "SQLite3" };
52 if params.driver == "SQLite3" then
53 params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
54 end
56 assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");
58 assert(connect());
59 end
61 local function getsql(sql, ...)
62 if params.driver == "PostgreSQL" then
63 sql = sql:gsub("`", "\"");
64 end
65 if not test_connection() then connect(); end
66 -- do prepared statement stuff
67 local stmt, err = connection:prepare(sql);
68 if not stmt and not test_connection() then error("connection failed"); end
69 if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
70 -- run query
71 local ok, err = stmt:execute(...);
72 if not ok and not test_connection() then error("connection failed"); end
73 if not ok then return nil, err; end
75 return stmt;
76 end
77 local function setsql(sql, ...)
78 local stmt, err = getsql(sql, ...);
79 if not stmt then return stmt, err; end
80 return stmt:affected();
81 end
83 local function get_password(username)
84 local stmt, err = getsql("SELECT `user_password` FROM `phpbb_users` WHERE `username_clean`=?", username);
85 if stmt then
86 for row in stmt:rows(true) do
87 return row.user_password;
88 end
89 end
90 end
91 local function check_sessionids(username, session_id)
92 -- TODO add session expiration and auto-login check
93 local stmt, err = getsql("SELECT phpbb_sessions.session_id FROM phpbb_sessions INNER JOIN phpbb_users ON phpbb_users.user_id = phpbb_sessions.session_user_id WHERE phpbb_users.username_clean =?", username);
94 if stmt then
95 for row in stmt:rows(true) do
96 -- if row.session_id == session_id then return true; end
98 -- workaround for possible LuaDBI bug
99 -- The session_id returned by the sql statement has an additional zero at the end. But that is not in the database.
100 if row.session_id == session_id or row.session_id == session_id.."0" then return true; end
106 local itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
107 local function hashEncode64(input, count)
108 local output = "";
109 local i, value = 0, 0;
111 while true do
112 value = input:byte(i+1)
113 i = i+1;
114 local idx = value % 0x40 + 1;
115 output = output .. itoa64:sub(idx, idx);
117 if i < count then
118 value = value + input:byte(i+1) * 256;
120 local _ = value % (2^6);
121 local idx = ((value - _) / (2^6)) % 0x40 + 1
122 output = output .. itoa64:sub(idx, idx);
124 if i >= count then break; end
125 i = i+1;
127 if i < count then
128 value = value + input:byte(i+1) * 256 * 256;
130 local _ = value % (2^12);
131 local idx = ((value - _) / (2^12)) % 0x40 + 1
132 output = output .. itoa64:sub(idx, idx);
134 if i >= count then break; end
135 i = i+1;
137 local _ = value % (2^18);
138 local idx = ((value - _) / (2^18)) % 0x40 + 1
139 output = output .. itoa64:sub(idx, idx);
141 if not(i < count) then break; end
143 return output;
145 local function hashCryptPrivate(password, genSalt)
146 local output = "*";
147 if not genSalt:match("^%$H%$") then return output; end
149 local count_log2 = itoa64:find(genSalt:sub(4,4)) - 1;
150 if count_log2 < 7 or count_log2 > 30 then return output; end
152 local count = 2 ^ count_log2;
153 local salt = genSalt:sub(5, 12);
155 if #salt ~= 8 then return output; end
157 local hash = md5(salt..password);
159 while true do
160 hash = md5(hash..password);
161 if not(count > 1) then break; end
162 count = count-1;
165 output = genSalt:sub(1, 12);
166 output = output .. hashEncode64(hash, 16);
168 return output;
170 local function hashGensaltPrivate(input)
171 local iteration_count_log2 = 6;
172 local output = "$H$";
173 local idx = math.min(iteration_count_log2 + 5, 30) + 1;
174 output = output .. itoa64:sub(idx, idx);
175 output = output .. hashEncode64(input, 6);
176 return output;
178 local function phpbbCheckHash(password, hash)
179 if #hash == 32 then return hash == md5(password, true); end -- legacy PHPBB2 hash
180 if #hash == 34 then return hashCryptPrivate(password, hash) == hash; end
181 if #hash == 60 and have_bcrypt then return bcrypt.verify(password, hash); end
182 module:log("error", "Unsupported hash: %s", hash);
183 return false;
185 local function phpbbCreateHash(password)
186 local random = uuid_gen():sub(-6);
187 local salt = hashGensaltPrivate(random);
188 local hash = hashCryptPrivate(password, salt);
189 if #hash == 34 then return hash; end
190 return md5(password, true);
194 provider = {};
196 function provider.test_password(username, password)
197 local hash = get_password(username);
198 return hash and phpbbCheckHash(password, hash);
200 function provider.user_exists(username)
201 module:log("debug", "test user %s existence", username);
202 return get_password(username) and true;
205 function provider.get_password(username)
206 return nil, "Getting password is not supported.";
208 function provider.set_password(username, password)
209 local hash = phpbbCreateHash(password);
210 local stmt, err = setsql("UPDATE `phpbb_users` SET `user_password`=? WHERE `username_clean`=?", hash, username);
211 return stmt and true, err;
213 function provider.create_user(username, password)
214 return nil, "Account creation/modification not supported.";
217 local escapes = {
218 [" "] = "\\20";
219 ['"'] = "\\22";
220 ["&"] = "\\26";
221 ["'"] = "\\27";
222 ["/"] = "\\2f";
223 [":"] = "\\3a";
224 ["<"] = "\\3c";
225 [">"] = "\\3e";
226 ["@"] = "\\40";
227 ["\\"] = "\\5c";
229 local unescapes = {};
230 for k,v in pairs(escapes) do unescapes[v] = k; end
231 local function jid_escape(s) return s and (s:gsub(".", escapes)); end
232 local function jid_unescape(s) return s and (s:gsub("\\%x%x", unescapes)); end
234 function provider.get_sasl_handler()
235 local sasl = {};
236 function sasl:clean_clone() return provider.get_sasl_handler(); end
237 function sasl:mechanisms() return { PLAIN = true; }; end
238 function sasl:select(mechanism)
239 if not self.selected and mechanism == "PLAIN" then
240 self.selected = mechanism;
241 return true;
244 function sasl:process(message)
245 if not message then return "failure", "malformed-request"; end
246 local authorization, authentication, password = message:match("^([^%z]*)%z([^%z]+)%z([^%z]+)");
247 if not authorization then return "failure", "malformed-request"; end
248 authentication = saslprep(authentication);
249 password = saslprep(password);
250 if (not password) or (password == "") or (not authentication) or (authentication == "") then
251 return "failure", "malformed-request", "Invalid username or password.";
253 local function test(authentication)
254 local prepped = nodeprep(authentication);
255 local normalized = jid_unescape(prepped);
256 return normalized and provider.test_password(normalized, password) and prepped;
258 local username = test(authentication) or test(jid_escape(authentication));
259 if not username and params.sessionid_as_password then
260 local function test(authentication)
261 local prepped = nodeprep(authentication);
262 local normalized = jid_unescape(prepped);
263 return normalized and check_sessionids(normalized, password) and prepped;
265 username = test(authentication) or test(jid_escape(authentication));
267 if username then
268 self.username = username;
269 return "success";
271 return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent.";
273 return sasl;
276 module:provides("auth", provider);