blob: 3bab56bb1d2b528ea4f50456424fe58367940791 [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2use PHPMailer\PHPMailer\PHPMailer;
3use PHPMailer\PHPMailer\SMTP;
4use PHPMailer\PHPMailer\Exception;
5function is_valid_regex($exp) {
6 return @preg_match($exp, '') !== false;
7}
8function isset_has_content($var) {
9 if (isset($var) && $var != "") {
10 return true;
11 }
12 else {
13 return false;
14 }
15}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020016function readable_random_string($length = 8) {
17 $string = '';
18 $vowels = array('a', 'e', 'i', 'o', 'u');
19 $consonants = array('b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z');
20 $max = $length / 2;
21 for ($i = 1; $i <= $max; $i++) {
22 $string .= $consonants[rand(0,19)];
23 $string .= $vowels[rand(0,4)];
24 }
25 return $string;
26}
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010027// Validates ips and cidrs
28function valid_network($network) {
29 if (filter_var($network, FILTER_VALIDATE_IP)) {
30 return true;
31 }
32 $parts = explode('/', $network);
33 if (count($parts) != 2) {
34 return false;
35 }
36 $ip = $parts[0];
37 $netmask = $parts[1];
38 if (!preg_match("/^\d+$/", $netmask)){
39 return false;
40 }
41 $netmask = intval($parts[1]);
42 if ($netmask < 0) {
43 return false;
44 }
45 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
46 return $netmask <= 32;
47 }
48 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
49 return $netmask <= 128;
50 }
51 return false;
52}
53function valid_hostname($hostname) {
54 return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
55}
56// Thanks to https://stackoverflow.com/a/49373789
57// Validates exact ip matches and ip-in-cidr, ipv4 and ipv6
58function ip_acl($ip, $networks) {
59 foreach($networks as $network) {
60 if (filter_var($network, FILTER_VALIDATE_IP)) {
61 if ($ip == $network) {
62 return true;
63 }
64 else {
65 continue;
66 }
67 }
68 $ipb = inet_pton($ip);
69 $iplen = strlen($ipb);
70 if (strlen($ipb) < 4) {
71 continue;
72 }
73 $ar = explode('/', $network);
74 $ip1 = $ar[0];
75 $ip1b = inet_pton($ip1);
76 $ip1len = strlen($ip1b);
77 if ($ip1len != $iplen) {
78 continue;
79 }
80 if (count($ar)>1) {
81 $bits=(int)($ar[1]);
82 }
83 else {
84 $bits = $iplen * 8;
85 }
86 for ($c=0; $bits>0; $c++) {
87 $bytemask = ($bits < 8) ? 0xff ^ ((1 << (8-$bits))-1) : 0xff;
88 if (((ord($ipb[$c]) ^ ord($ip1b[$c])) & $bytemask) != 0) {
89 continue 2;
90 }
91 $bits-=8;
92 }
93 return true;
94 }
95 return false;
96}
97function hash_password($password) {
98 // default_pass_scheme is determined in vars.inc.php (or corresponding local file)
99 // in case default pass scheme is not defined, falling back to BLF-CRYPT.
100 global $default_pass_scheme;
101 $pw_hash = NULL;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200102 // support pre-hashed passwords
103 if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
104 $pw_hash = $password;
105 }
106 else {
107 switch (strtoupper($default_pass_scheme)) {
108 case "SSHA":
109 $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
110 $pw_hash = "{SSHA}".base64_encode(hash('sha1', $password . $salt_str, true) . $salt_str);
111 break;
112 case "SSHA256":
113 $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
114 $pw_hash = "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
115 break;
116 case "SSHA512":
117 $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
118 $pw_hash = "{SSHA512}".base64_encode(hash('sha512', $password . $salt_str, true) . $salt_str);
119 break;
120 case "BLF-CRYPT":
121 default:
122 $pw_hash = "{BLF-CRYPT}" . password_hash($password, PASSWORD_BCRYPT);
123 break;
124 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100125 }
126 return $pw_hash;
127}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200128function password_complexity($_action, $_data = null) {
129 global $redis;
130 global $lang;
131 switch ($_action) {
132 case 'edit':
133 if ($_SESSION['mailcow_cc_role'] != "admin") {
134 $_SESSION['return'][] = array(
135 'type' => 'danger',
136 'log' => array(__FUNCTION__, $_action, $_data),
137 'msg' => 'access_denied'
138 );
139 return false;
140 }
141 $is_now = password_complexity('get');
142 if (!empty($is_now)) {
143 $length = (isset($_data['length']) && intval($_data['length']) >= 3) ? intval($_data['length']) : $is_now['length'];
144 $chars = (isset($_data['chars'])) ? intval($_data['chars']) : $is_now['chars'];
145 $lowerupper = (isset($_data['lowerupper'])) ? intval($_data['lowerupper']) : $is_now['lowerupper'];
146 $special_chars = (isset($_data['special_chars'])) ? intval($_data['special_chars']) : $is_now['special_chars'];
147 $numbers = (isset($_data['numbers'])) ? intval($_data['numbers']) : $is_now['numbers'];
148 }
149 try {
150 $redis->hMSet('PASSWD_POLICY', [
151 'length' => $length,
152 'chars' => $chars,
153 'special_chars' => $special_chars,
154 'lowerupper' => $lowerupper,
155 'numbers' => $numbers
156 ]);
157 }
158 catch (RedisException $e) {
159 $_SESSION['return'][] = array(
160 'type' => 'danger',
161 'log' => array(__FUNCTION__, $_action, $_data),
162 'msg' => array('redis_error', $e)
163 );
164 return false;
165 }
166 $_SESSION['return'][] = array(
167 'type' => 'success',
168 'log' => array(__FUNCTION__, $_action, $_data),
169 'msg' => 'password_policy_saved'
170 );
171 break;
172 case 'get':
173 try {
174 $length = $redis->hGet('PASSWD_POLICY', 'length');
175 $chars = $redis->hGet('PASSWD_POLICY', 'chars');
176 $special_chars = $redis->hGet('PASSWD_POLICY', 'special_chars');
177 $lowerupper = $redis->hGet('PASSWD_POLICY', 'lowerupper');
178 $numbers = $redis->hGet('PASSWD_POLICY', 'numbers');
179 return array(
180 'length' => $length,
181 'chars' => $chars,
182 'special_chars' => $special_chars,
183 'lowerupper' => $lowerupper,
184 'numbers' => $numbers
185 );
186 }
187 catch (RedisException $e) {
188 $_SESSION['return'][] = array(
189 'type' => 'danger',
190 'log' => array(__FUNCTION__, $_action, $_data),
191 'msg' => array('redis_error', $e)
192 );
193 return false;
194 }
195 return false;
196 break;
197 case 'html':
198 $policies = password_complexity('get');
199 foreach ($policies as $name => $value) {
200 if ($value != 0) {
201 $policy_text[] = sprintf($lang['admin']["password_policy_$name"], $value);
202 }
203 }
204 return '<p class="help-block small">- ' . implode('<br>- ', (array)$policy_text) . '</p>';
205 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100206 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200207}
208function password_check($password1, $password2) {
209 $password_complexity = password_complexity('get');
210
211 if (empty($password1) || empty($password2)) {
212 $_SESSION['return'][] = array(
213 'type' => 'danger',
214 'log' => array(__FUNCTION__, $_action, $_type),
215 'msg' => 'password_complexity'
216 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100217 return false;
218 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200219
220 if ($password1 != $password2) {
221 $_SESSION['return'][] = array(
222 'type' => 'danger',
223 'log' => array(__FUNCTION__, $_action, $_type),
224 'msg' => 'password_mismatch'
225 );
226 return false;
227 }
228
229 $given_password['length'] = strlen($password1);
230 $given_password['special_chars'] = preg_match('/[^a-zA-Z\d]/', $password1);
231 $given_password['chars'] = preg_match('/[a-zA-Z]/',$password1);
232 $given_password['numbers'] = preg_match('/\d/', $password1);
233 $lower = strlen(preg_replace("/[^a-z]/", '', $password1));
234 $upper = strlen(preg_replace("/[^A-Z]/", '', $password1));
235 $given_password['lowerupper'] = ($lower > 0 && $upper > 0) ? true : false;
236
237 if (
238 ($given_password['length'] < $password_complexity['length']) ||
239 ($password_complexity['special_chars'] == 1 && (intval($given_password['special_chars']) != $password_complexity['special_chars'])) ||
240 ($password_complexity['chars'] == 1 && (intval($given_password['chars']) != $password_complexity['chars'])) ||
241 ($password_complexity['numbers'] == 1 && (intval($given_password['numbers']) != $password_complexity['numbers'])) ||
242 ($password_complexity['lowerupper'] == 1 && (intval($given_password['lowerupper']) != $password_complexity['lowerupper']))
243 ) {
244 $_SESSION['return'][] = array(
245 'type' => 'danger',
246 'log' => array(__FUNCTION__, $_action, $_type),
247 'msg' => 'password_complexity'
248 );
249 return false;
250 }
251
252 return true;
253}
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100254function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200255 global $pdo;
256 global $redis;
257 $sasl_limit_days = intval($sasl_limit_days);
258 switch ($action) {
259 case 'get':
260 if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100261 $stmt = $pdo->prepare('SELECT `real_rip`, MAX(`datetime`) as `datetime`, `service`, `app_password`, MAX(`app_passwd`.`name`) as `app_password_name` FROM `sasl_log`
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200262 LEFT OUTER JOIN `app_passwd` on `sasl_log`.`app_password` = `app_passwd`.`id`
263 WHERE `username` = :username
264 AND HOUR(TIMEDIFF(NOW(), `datetime`)) < :sasl_limit_days
265 GROUP BY `real_rip`, `service`, `app_password`
266 ORDER BY `datetime` DESC;');
267 $stmt->execute(array(':username' => $username, ':sasl_limit_days' => ($sasl_limit_days * 24)));
268 $sasl = $stmt->fetchAll(PDO::FETCH_ASSOC);
269 foreach ($sasl as $k => $v) {
270 if (!filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
271 $sasl[$k]['real_rip'] = 'Web/EAS/Internal (' . $sasl[$k]['real_rip'] . ')';
272 }
273 elseif (filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
274 try {
275 $sasl[$k]['location'] = $redis->hGet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip']);
276 }
277 catch (RedisException $e) {
278 $_SESSION['return'][] = array(
279 'type' => 'danger',
280 'log' => array(__FUNCTION__, $_action, $_data_log),
281 'msg' => array('redis_error', $e)
282 );
283 return false;
284 }
285 if (!$sasl[$k]['location']) {
286 $curl = curl_init();
287 curl_setopt($curl, CURLOPT_URL,"https://dfdata.bella.network/lookup/" . $sasl[$k]['real_rip']);
288 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
289 curl_setopt($curl, CURLOPT_USERAGENT, 'Moocow');
290 curl_setopt($curl, CURLOPT_TIMEOUT, 5);
291 $ip_data = curl_exec($curl);
292 if (!curl_errno($curl)) {
293 $ip_data_array = json_decode($ip_data, true);
294 if ($ip_data_array !== false and !empty($ip_data_array['location']['shortcountry'])) {
295 $sasl[$k]['location'] = $ip_data_array['location']['shortcountry'];
296 try {
297 $redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['location']['shortcountry']);
298 }
299 catch (RedisException $e) {
300 $_SESSION['return'][] = array(
301 'type' => 'danger',
302 'log' => array(__FUNCTION__, $_action, $_data_log),
303 'msg' => array('redis_error', $e)
304 );
305 curl_close($curl);
306 return false;
307 }
308 }
309 }
310 curl_close($curl);
311 }
312 }
313 }
314 }
315 else {
316 $sasl = array();
317 }
318 if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) {
319 $stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs`
320 WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
321 AND JSON_EXTRACT(`call`, "$[1]") = :username
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100322 AND `type` = "success" ORDER BY `time` DESC LIMIT 1 OFFSET :offset');
323 $stmt->execute(array(
324 ':username' => $username,
325 ':offset' => $ui_offset
326 ));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200327 $ui = $stmt->fetch(PDO::FETCH_ASSOC);
328 }
329 else {
330 $ui = array();
331 }
332
333 return array('ui' => $ui, 'sasl' => $sasl);
334 break;
335 case 'reset':
336 if (filter_var($username, FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
337 $stmt = $pdo->prepare('DELETE FROM `sasl_log`
338 WHERE `username` = :username');
339 $stmt->execute(array(':username' => $username));
340 }
341 if ($_SESSION['mailcow_cc_role'] == "admin" || $username == $_SESSION['mailcow_cc_username']) {
342 $stmt = $pdo->prepare('DELETE FROM `logs`
343 WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
344 AND JSON_EXTRACT(`call`, "$[1]") = :username
345 AND `type` = "success"');
346 $stmt->execute(array(':username' => $username));
347 }
348 return true;
349 break;
350 }
351
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100352}
353function flush_memcached() {
354 try {
355 $m = new Memcached();
356 $m->addServer('memcached', 11211);
357 $m->flush();
358 }
359 catch ( Exception $e ) {
360 // Dunno
361 }
362}
363function sys_mail($_data) {
364 if ($_SESSION['mailcow_cc_role'] != "admin") {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200365 $_SESSION['return'][] = array(
366 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100367 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200368 'msg' => 'access_denied'
369 );
370 return false;
371 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100372 $excludes = $_data['mass_exclude'];
373 $includes = $_data['mass_include'];
374 $mailboxes = array();
375 $mass_from = $_data['mass_from'];
376 $mass_text = $_data['mass_text'];
377 $mass_html = $_data['mass_html'];
378 $mass_subject = $_data['mass_subject'];
379 if (!filter_var($mass_from, FILTER_VALIDATE_EMAIL)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200380 $_SESSION['return'][] = array(
381 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100382 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200383 'msg' => 'from_invalid'
384 );
385 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100386 }
387 if (empty($mass_subject)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200388 $_SESSION['return'][] = array(
389 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100390 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200391 'msg' => 'subject_empty'
392 );
393 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100394 }
395 if (empty($mass_text)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200396 $_SESSION['return'][] = array(
397 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100398 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200399 'msg' => 'text_empty'
400 );
401 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100402 }
403 $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
404 foreach ($domains as $domain) {
405 foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
406 $mailboxes[] = $mailbox;
407 }
408 }
409 if (!empty($includes)) {
410 $rcpts = array_intersect($mailboxes, $includes);
411 }
412 elseif (!empty($excludes)) {
413 $rcpts = array_diff($mailboxes, $excludes);
414 }
415 else {
416 $rcpts = $mailboxes;
417 }
418 if (!empty($rcpts)) {
419 ini_set('max_execution_time', 0);
420 ini_set('max_input_time', 0);
421 $mail = new PHPMailer;
422 $mail->Timeout = 10;
423 $mail->SMTPOptions = array(
424 'ssl' => array(
425 'verify_peer' => false,
426 'verify_peer_name' => false,
427 'allow_self_signed' => true
428 )
429 );
430 $mail->isSMTP();
431 $mail->Host = 'dovecot-mailcow';
432 $mail->SMTPAuth = false;
433 $mail->Port = 24;
434 $mail->setFrom($mass_from);
435 $mail->Subject = $mass_subject;
436 $mail->CharSet ="UTF-8";
437 if (!empty($mass_html)) {
438 $mail->Body = $mass_html;
439 $mail->AltBody = $mass_text;
440 }
441 else {
442 $mail->Body = $mass_text;
443 }
444 $mail->XMailer = 'MooMassMail';
445 foreach ($rcpts as $rcpt) {
446 $mail->AddAddress($rcpt);
447 if (!$mail->send()) {
448 $_SESSION['return'][] = array(
449 'type' => 'warning',
450 'log' => array(__FUNCTION__),
451 'msg' => 'Mailer error (RCPT "' . htmlspecialchars($rcpt) . '"): ' . str_replace('https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting', '', $mail->ErrorInfo)
452 );
453 }
454 $mail->ClearAllRecipients();
455 }
456 }
457 $_SESSION['return'][] = array(
458 'type' => 'success',
459 'log' => array(__FUNCTION__),
460 'msg' => 'Mass mail job completed, sent ' . count($rcpts) . ' mails'
461 );
462}
463function logger($_data = false) {
464 /*
465 logger() will be called as last function
466 To manually log a message, logger needs to be called like below.
467
468 logger(array(
469 'return' => array(
470 array(
471 'type' => 'danger',
472 'log' => array(__FUNCTION__),
473 'msg' => $err
474 )
475 )
476 ));
477
478 These messages will not be printed as alert box.
479 To do so, push them to $_SESSION['return'] and do not call logger as they will be included automatically:
480
481 $_SESSION['return'][] = array(
482 'type' => 'danger',
483 'log' => array(__FUNCTION__, $user, '*'),
484 'msg' => $err
485 );
486 */
487 global $pdo;
488 if (!$_data) {
489 $_data = $_SESSION;
490 }
491 if (!empty($_data['return'])) {
492 $task = substr(strtoupper(md5(uniqid(rand(), true))), 0, 6);
493 foreach ($_data['return'] as $return) {
494 $type = $return['type'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200495 $msg = null;
496 if (isset($return['msg'])) {
497 $msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE);
498 }
499 $call = null;
500 if (isset($return['log'])) {
501 $call = json_encode($return['log'], JSON_UNESCAPED_UNICODE);
502 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100503 if (!empty($_SESSION["dual-login"]["username"])) {
504 $user = $_SESSION["dual-login"]["username"] . ' => ' . $_SESSION['mailcow_cc_username'];
505 $role = $_SESSION["dual-login"]["role"] . ' => ' . $_SESSION['mailcow_cc_role'];
506 }
507 elseif (!empty($_SESSION['mailcow_cc_username'])) {
508 $user = $_SESSION['mailcow_cc_username'];
509 $role = $_SESSION['mailcow_cc_role'];
510 }
511 else {
512 $user = 'unauthenticated';
513 $role = 'unauthenticated';
514 }
515 // We cannot log when logs is missing...
516 try {
517 $stmt = $pdo->prepare("INSERT INTO `logs` (`type`, `task`, `msg`, `call`, `user`, `role`, `remote`, `time`) VALUES
518 (:type, :task, :msg, :call, :user, :role, :remote, UNIX_TIMESTAMP())");
519 $stmt->execute(array(
520 ':type' => $type,
521 ':task' => $task,
522 ':call' => $call,
523 ':msg' => $msg,
524 ':user' => $user,
525 ':role' => $role,
526 ':remote' => get_remote_ip()
527 ));
528 }
529 catch (Exception $e) {
530 // Do nothing
531 }
532 }
533 }
534 else {
535 return true;
536 }
537}
538function hasDomainAccess($username, $role, $domain) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200539 global $pdo;
540 if (!filter_var($username, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
541 return false;
542 }
543 if (empty($domain) || !is_valid_domain_name($domain)) {
544 return false;
545 }
546 if ($role != 'admin' && $role != 'domainadmin') {
547 return false;
548 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100549 if ($role == 'admin') {
550 $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
551 WHERE `domain` = :domain");
552 $stmt->execute(array(':domain' => $domain));
553 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
554 $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
555 WHERE `alias_domain` = :domain");
556 $stmt->execute(array(':domain' => $domain));
557 $num_results = $num_results + count($stmt->fetchAll(PDO::FETCH_ASSOC));
558 if ($num_results != 0) {
559 return true;
560 }
561 }
562 elseif ($role == 'domainadmin') {
563 $stmt = $pdo->prepare("SELECT `domain` FROM `domain_admins`
564 WHERE (
565 `active`='1'
566 AND `username` = :username
567 AND (`domain` = :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2))
568 )");
569 $stmt->execute(array(':username' => $username, ':domain1' => $domain, ':domain2' => $domain));
570 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
571 if (!empty($num_results)) {
572 return true;
573 }
574 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200575 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100576}
577function hasMailboxObjectAccess($username, $role, $object) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200578 global $pdo;
579 if (empty($username) || empty($role) || empty($object)) {
580 return false;
581 }
582 if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
583 return false;
584 }
585 if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
586 return false;
587 }
588 if ($username == $object) {
589 return true;
590 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100591 $stmt = $pdo->prepare("SELECT `domain` FROM `mailbox` WHERE `username` = :object");
592 $stmt->execute(array(':object' => $object));
593 $row = $stmt->fetch(PDO::FETCH_ASSOC);
594 if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
595 return true;
596 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200597 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100598}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200599// does also verify mailboxes as a mailbox is a alias == goto
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100600function hasAliasObjectAccess($username, $role, $object) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200601 global $pdo;
602 if (empty($username) || empty($role) || empty($object)) {
603 return false;
604 }
605 if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
606 return false;
607 }
608 if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
609 return false;
610 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100611 $stmt = $pdo->prepare("SELECT `domain` FROM `alias` WHERE `address` = :object");
612 $stmt->execute(array(':object' => $object));
613 $row = $stmt->fetch(PDO::FETCH_ASSOC);
614 if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
615 return true;
616 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200617 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100618}
619function pem_to_der($pem_key) {
620 // Need to remove BEGIN/END PUBLIC KEY
621 $lines = explode("\n", trim($pem_key));
622 unset($lines[count($lines)-1]);
623 unset($lines[0]);
624 return base64_decode(implode('', $lines));
625}
626function expand_ipv6($ip) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200627 $hex = unpack("H*hex", inet_pton($ip));
628 $ip = substr(preg_replace("/([A-f0-9]{4})/", "$1:", $hex['hex']), 0, -1);
629 return $ip;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100630}
631function generate_tlsa_digest($hostname, $port, $starttls = null) {
632 if (!is_valid_domain_name($hostname)) {
633 return "Not a valid hostname";
634 }
635 if (empty($starttls)) {
636 $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true)));
637 $stream = stream_socket_client('ssl://' . $hostname . ':' . $port, $error_nr, $error_msg, 5, STREAM_CLIENT_CONNECT, $context);
638 if (!$stream) {
639 $error_msg = isset($error_msg) ? $error_msg : '-';
640 return $error_nr . ': ' . $error_msg;
641 }
642 }
643 else {
644 $stream = stream_socket_client('tcp://' . $hostname . ':' . $port, $error_nr, $error_msg, 5);
645 if (!$stream) {
646 return $error_nr . ': ' . $error_msg;
647 }
648 $banner = fread($stream, 512 );
649 if (preg_match("/^220/i", $banner)) { // SMTP
650 fwrite($stream,"HELO tlsa.generator.local\r\n");
651 fread($stream, 512);
652 fwrite($stream,"STARTTLS\r\n");
653 fread($stream, 512);
654 }
655 elseif (preg_match("/imap.+starttls/i", $banner)) { // IMAP
656 fwrite($stream,"A1 STARTTLS\r\n");
657 fread($stream, 512);
658 }
659 elseif (preg_match("/^\+OK/", $banner)) { // POP3
660 fwrite($stream,"STLS\r\n");
661 fread($stream, 512);
662 }
663 elseif (preg_match("/^OK/m", $banner)) { // Sieve
664 fwrite($stream,"STARTTLS\r\n");
665 fread($stream, 512);
666 }
667 else {
668 return 'Unknown banner: "' . htmlspecialchars(trim($banner)) . '"';
669 }
670 // Upgrade connection
671 stream_set_blocking($stream, true);
672 stream_context_set_option($stream, 'ssl', 'capture_peer_cert', true);
673 stream_context_set_option($stream, 'ssl', 'verify_peer', false);
674 stream_context_set_option($stream, 'ssl', 'verify_peer_name', false);
675 stream_context_set_option($stream, 'ssl', 'allow_self_signed', true);
676 stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT);
677 stream_set_blocking($stream, false);
678 }
679 $params = stream_context_get_params($stream);
680 if (!empty($params['options']['ssl']['peer_certificate'])) {
681 $key_resource = openssl_pkey_get_public($params['options']['ssl']['peer_certificate']);
682 // We cannot get ['rsa']['n'], the binary data would contain BEGIN/END PUBLIC KEY
683 $key_data = openssl_pkey_get_details($key_resource)['key'];
684 return '3 1 1 ' . openssl_digest(pem_to_der($key_data), 'sha256');
685 }
686 else {
687 return 'Error: Cannot read peer certificate';
688 }
689}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200690function alertbox_log_parser($_data) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100691 global $lang;
692 if (isset($_data['return'])) {
693 foreach ($_data['return'] as $return) {
694 // Get type
695 $type = $return['type'];
696 // If a lang[type][msg] string exists, use it as message
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200697 if (isset($return['type']) && isset($return['msg']) && !is_array($return['msg'])) {
698 if (isset($lang[$return['type']][$return['msg']])) {
699 $msg = $lang[$return['type']][$return['msg']];
700 }
701 else {
702 $msg = $return['msg'];
703 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100704 }
705 // If msg is an array, use first element as language string and run printf on it with remaining array elements
706 elseif (is_array($return['msg'])) {
707 $msg = array_shift($return['msg']);
708 $msg = vsprintf(
709 $lang[$return['type']][$msg],
710 $return['msg']
711 );
712 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100713 else {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200714 $msg = '-';
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100715 }
716 $log_array[] = array('msg' => $msg, 'type' => json_encode($type));
717 }
718 if (!empty($log_array)) {
719 return $log_array;
720 }
721 }
722 return false;
723}
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200724function verify_salted_hash($hash, $password, $algo, $salt_length) {
725 // Decode hash
726 $dhash = base64_decode($hash);
727 // Get first n bytes of binary which equals a SSHA hash
728 $ohash = substr($dhash, 0, $salt_length);
729 // Remove SSHA hash from decoded hash to get original salt string
730 $osalt = str_replace($ohash, '', $dhash);
731 // Check single salted SSHA hash against extracted hash
732 if (hash_equals(hash($algo, $password . $osalt, true), $ohash)) {
733 return true;
734 }
735 return false;
736}
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100737function verify_hash($hash, $password) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200738 if (preg_match('/^{(.+)}(.+)/i', $hash, $hash_array)) {
739 $scheme = strtoupper($hash_array[1]);
740 $hash = $hash_array[2];
741 switch ($scheme) {
742 case "ARGON2I":
743 case "ARGON2ID":
744 case "BLF-CRYPT":
745 case "CRYPT":
746 case "DES-CRYPT":
747 case "MD5-CRYPT":
748 case "MD5":
749 case "SHA256-CRYPT":
750 case "SHA512-CRYPT":
751 return password_verify($password, $hash);
752
753 case "CLEAR":
754 case "CLEARTEXT":
755 case "PLAIN":
756 return $password == $hash;
757
758 case "LDAP-MD5":
759 $hash = base64_decode($hash);
760 return hash_equals(hash('md5', $password, true), $hash);
761
762 case "PBKDF2":
763 $components = explode('$', $hash);
764 $salt = $components[2];
765 $rounds = $components[3];
766 $hash = $components[4];
767 return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
768
769 case "PLAIN-MD4":
770 return hash_equals(hash('md4', $password), $hash);
771
772 case "PLAIN-MD5":
773 return md5($password) == $hash;
774
775 case "PLAIN-TRUNC":
776 $components = explode('-', $hash);
777 if (count($components) > 1) {
778 $trunc_len = $components[0];
779 $trunc_password = $components[1];
780
781 return substr($password, 0, $trunc_len) == $trunc_password;
782 } else {
783 return $password == $hash;
784 }
785
786 case "SHA":
787 case "SHA1":
788 case "SHA256":
789 case "SHA512":
790 // SHA is an alias for SHA1
791 $scheme = $scheme == "SHA" ? "sha1" : strtolower($scheme);
792 $hash = base64_decode($hash);
793 return hash_equals(hash($scheme, $password, true), $hash);
794
795 case "SMD5":
796 return verify_salted_hash($hash, $password, 'md5', 16);
797
798 case "SSHA":
799 return verify_salted_hash($hash, $password, 'sha1', 20);
800
801 case "SSHA256":
802 return verify_salted_hash($hash, $password, 'sha256', 32);
803
804 case "SSHA512":
805 return verify_salted_hash($hash, $password, 'sha512', 64);
806
807 default:
808 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100809 }
810 }
811 return false;
812}
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100813function check_login($user, $pass, $app_passwd_data = false) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200814 global $pdo;
815 global $redis;
816 global $imap_server;
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100817
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200818 if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100819 $_SESSION['return'][] = array(
820 'type' => 'danger',
821 'log' => array(__FUNCTION__, $user, '*'),
822 'msg' => 'malformed_username'
823 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200824 return false;
825 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100826
827 // Validate admin
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200828 $user = strtolower(trim($user));
829 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
830 WHERE `superadmin` = '1'
831 AND `active` = '1'
832 AND `username` = :user");
833 $stmt->execute(array(':user' => $user));
834 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
835 foreach ($rows as $row) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100836 // verify password
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200837 if (verify_hash($row['password'], $pass)) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100838 // check for tfa authenticators
839 $authenticators = get_tfa($user);
840 if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
841 // active tfa authenticators found, set pending user login
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100842 $_SESSION['pending_mailcow_cc_username'] = $user;
843 $_SESSION['pending_mailcow_cc_role'] = "admin";
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100844 $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100845 unset($_SESSION['ldelay']);
846 $_SESSION['return'][] = array(
847 'type' => 'info',
848 'log' => array(__FUNCTION__, $user, '*'),
849 'msg' => 'awaiting_tfa_confirmation'
850 );
851 return "pending";
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100852 } else {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100853 unset($_SESSION['ldelay']);
854 // Reactivate TFA if it was set to "deactivate TFA for next login"
855 $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
856 $stmt->execute(array(':user' => $user));
857 $_SESSION['return'][] = array(
858 'type' => 'success',
859 'log' => array(__FUNCTION__, $user, '*'),
860 'msg' => array('logged_in_as', $user)
861 );
862 return "admin";
863 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200864 }
865 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100866
867 // Validate domain admin
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200868 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
869 WHERE `superadmin` = '0'
870 AND `active`='1'
871 AND `username` = :user");
872 $stmt->execute(array(':user' => $user));
873 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
874 foreach ($rows as $row) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100875 // verify password
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200876 if (verify_hash($row['password'], $pass) !== false) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100877 // check for tfa authenticators
878 $authenticators = get_tfa($user);
879 if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100880 $_SESSION['pending_mailcow_cc_username'] = $user;
881 $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100882 $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100883 unset($_SESSION['ldelay']);
884 $_SESSION['return'][] = array(
885 'type' => 'info',
886 'log' => array(__FUNCTION__, $user, '*'),
887 'msg' => 'awaiting_tfa_confirmation'
888 );
889 return "pending";
890 }
891 else {
892 unset($_SESSION['ldelay']);
893 // Reactivate TFA if it was set to "deactivate TFA for next login"
894 $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
895 $stmt->execute(array(':user' => $user));
896 $_SESSION['return'][] = array(
897 'type' => 'success',
898 'log' => array(__FUNCTION__, $user, '*'),
899 'msg' => array('logged_in_as', $user)
900 );
901 return "domainadmin";
902 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200903 }
904 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100905
906 // Validate mailbox user
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200907 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100908 INNER JOIN domain on mailbox.domain = domain.domain
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200909 WHERE `kind` NOT REGEXP 'location|thing|group'
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100910 AND `mailbox`.`active`='1'
911 AND `domain`.`active`='1'
912 AND `username` = :user");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200913 $stmt->execute(array(':user' => $user));
914 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100915 if ($app_passwd_data['eas'] === true) {
916 $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
917 INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
918 INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
919 WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
920 AND `mailbox`.`active` = '1'
921 AND `domain`.`active` = '1'
922 AND `app_passwd`.`active` = '1'
923 AND `app_passwd`.`eas_access` = '1'
924 AND `app_passwd`.`mailbox` = :user");
925 $stmt->execute(array(':user' => $user));
926 $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
927 }
928 elseif ($app_passwd_data['dav'] === true) {
929 $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
930 INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
931 INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
932 WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
933 AND `mailbox`.`active` = '1'
934 AND `domain`.`active` = '1'
935 AND `app_passwd`.`active` = '1'
936 AND `app_passwd`.`dav_access` = '1'
937 AND `app_passwd`.`mailbox` = :user");
938 $stmt->execute(array(':user' => $user));
939 $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
940 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100941 foreach ($rows as $row) {
942 // verify password
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200943 if (verify_hash($row['password'], $pass) !== false) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100944 if (!array_key_exists("app_passwd_id", $row)){
945 // password is not a app password
946 // check for tfa authenticators
947 $authenticators = get_tfa($user);
948 if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 &&
949 $app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true) {
950 // authenticators found, init TFA flow
951 $_SESSION['pending_mailcow_cc_username'] = $user;
952 $_SESSION['pending_mailcow_cc_role'] = "user";
953 $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
954 unset($_SESSION['ldelay']);
955 $_SESSION['return'][] = array(
956 'type' => 'success',
957 'log' => array(__FUNCTION__, $user, '*'),
958 'msg' => array('logged_in_as', $user)
959 );
960 return "pending";
961 } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
962 // no authenticators found, login successfull
963 // Reactivate TFA if it was set to "deactivate TFA for next login"
964 $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
965 $stmt->execute(array(':user' => $user));
966
967 unset($_SESSION['ldelay']);
968 return "user";
969 }
970 } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
971 // password is a app password
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100972 $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
973 $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
974 $stmt->execute(array(
975 ':service' => $service,
976 ':app_id' => $row['app_passwd_id'],
977 ':username' => $user,
978 ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
979 ));
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100980
981 unset($_SESSION['ldelay']);
982 return "user";
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100983 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200984 }
985 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100986
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200987 if (!isset($_SESSION['ldelay'])) {
988 $_SESSION['ldelay'] = "0";
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100989 $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
990 error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200991 }
992 elseif (!isset($_SESSION['mailcow_cc_username'])) {
993 $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100994 $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200995 error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
996 }
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +0100997
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100998 $_SESSION['return'][] = array(
999 'type' => 'danger',
1000 'log' => array(__FUNCTION__, $user, '*'),
1001 'msg' => 'login_failed'
1002 );
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001003
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001004 sleep($_SESSION['ldelay']);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001005 return false;
1006}
1007function formatBytes($size, $precision = 2) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001008 if(!is_numeric($size)) {
1009 return "0";
1010 }
1011 $base = log($size, 1024);
1012 $suffixes = array(' Byte', ' KiB', ' MiB', ' GiB', ' TiB');
1013 if ($size == "0") {
1014 return "0";
1015 }
1016 return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001017}
1018function update_sogo_static_view() {
1019 if (getenv('SKIP_SOGO') == "y") {
1020 return true;
1021 }
1022 global $pdo;
1023 global $lang;
1024 $stmt = $pdo->query("SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES
1025 WHERE TABLE_NAME = 'sogo_view'");
1026 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
1027 if ($num_results != 0) {
1028 $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
1029 SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view");
1030 $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
1031 }
1032 flush_memcached();
1033}
1034function edit_user_account($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001035 global $lang;
1036 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001037 $_data_log = $_data;
1038 !isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
1039 !isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
1040 !isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
1041 $username = $_SESSION['mailcow_cc_username'];
1042 $role = $_SESSION['mailcow_cc_role'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001043 $password_old = $_data['user_old_pass'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001044 if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') {
1045 $_SESSION['return'][] = array(
1046 'type' => 'danger',
1047 'log' => array(__FUNCTION__, $_data_log),
1048 'msg' => 'access_denied'
1049 );
1050 return false;
1051 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001052 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
1053 WHERE `kind` NOT REGEXP 'location|thing|group'
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001054 AND `username` = :user");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001055 $stmt->execute(array(':user' => $username));
1056 $row = $stmt->fetch(PDO::FETCH_ASSOC);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001057 if (!verify_hash($row['password'], $password_old)) {
1058 $_SESSION['return'][] = array(
1059 'type' => 'danger',
1060 'log' => array(__FUNCTION__, $_data_log),
1061 'msg' => 'access_denied'
1062 );
1063 return false;
1064 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001065 if (!empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
1066 $password_new = $_data['user_new_pass'];
1067 $password_new2 = $_data['user_new_pass2'];
1068 if (password_check($password_new, $password_new2) !== true) {
1069 return false;
1070 }
1071 $password_hashed = hash_password($password_new);
1072 $stmt = $pdo->prepare("UPDATE `mailbox` SET `password` = :password_hashed,
1073 `attributes` = JSON_SET(`attributes`, '$.force_pw_update', '0'),
1074 `attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW())
1075 WHERE `username` = :username");
1076 $stmt->execute(array(
1077 ':password_hashed' => $password_hashed,
1078 ':username' => $username
1079 ));
1080 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001081 update_sogo_static_view();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001082 $_SESSION['return'][] = array(
1083 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001084 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001085 'msg' => array('mailbox_modified', htmlspecialchars($username))
1086 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001087}
1088function user_get_alias_details($username) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001089 global $pdo;
1090 global $lang;
1091 $data['direct_aliases'] = array();
1092 $data['shared_aliases'] = array();
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001093 if ($_SESSION['mailcow_cc_role'] == "user") {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001094 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001095 }
1096 if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
1097 return false;
1098 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001099 if (!hasMailboxObjectAccess($username, $_SESSION['mailcow_cc_role'], $username)) {
1100 return false;
1101 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001102 $data['address'] = $username;
1103 $stmt = $pdo->prepare("SELECT `address` AS `shared_aliases`, `public_comment` FROM `alias`
1104 WHERE `goto` REGEXP :username_goto
1105 AND `address` NOT LIKE '@%'
1106 AND `goto` != :username_goto2
1107 AND `address` != :username_address");
1108 $stmt->execute(array(
1109 ':username_goto' => '(^|,)'.$username.'($|,)',
1110 ':username_goto2' => $username,
1111 ':username_address' => $username
1112 ));
1113 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1114 while ($row = array_shift($run)) {
1115 $data['shared_aliases'][$row['shared_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001116 //$data['shared_aliases'][] = $row['shared_aliases'];
1117 }
1118
1119 $stmt = $pdo->prepare("SELECT `address` AS `direct_aliases`, `public_comment` FROM `alias`
1120 WHERE `goto` = :username_goto
1121 AND `address` NOT LIKE '@%'
1122 AND `address` != :username_address");
1123 $stmt->execute(
1124 array(
1125 ':username_goto' => $username,
1126 ':username_address' => $username
1127 ));
1128 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1129 while ($row = array_shift($run)) {
1130 $data['direct_aliases'][$row['direct_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
1131 }
1132 $stmt = $pdo->prepare("SELECT CONCAT(local_part, '@', alias_domain) AS `ad_alias`, `alias_domain` FROM `mailbox`
1133 LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain`
1134 WHERE `username` = :username ;");
1135 $stmt->execute(array(':username' => $username));
1136 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1137 while ($row = array_shift($run)) {
1138 if (empty($row['ad_alias'])) {
1139 continue;
1140 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001141 $data['direct_aliases'][$row['ad_alias']]['public_comment'] = $lang['add']['alias_domain'];
1142 $data['alias_domains'][] = $row['alias_domain'];
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001143 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001144 $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`send_as` SEPARATOR ', '), '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` NOT LIKE '@%';");
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001145 $stmt->execute(array(':username' => $username));
1146 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1147 while ($row = array_shift($run)) {
1148 $data['aliases_also_send_as'] = $row['send_as'];
1149 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001150 $stmt = $pdo->prepare("SELECT CONCAT_WS(', ', IFNULL(GROUP_CONCAT(DISTINCT `send_as` SEPARATOR ', '), ''), GROUP_CONCAT(DISTINCT CONCAT('@',`alias_domain`) SEPARATOR ', ')) AS `send_as` FROM `sender_acl` LEFT JOIN `alias_domain` ON `alias_domain`.`target_domain` = TRIM(LEADING '@' FROM `send_as`) WHERE `logged_in_as` = :username AND `send_as` LIKE '@%';");
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001151 $stmt->execute(array(':username' => $username));
1152 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1153 while ($row = array_shift($run)) {
1154 $data['aliases_send_as_all'] = $row['send_as'];
1155 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001156 $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '') as `address` FROM `alias` WHERE `goto` REGEXP :username AND `address` LIKE '@%';");
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001157 $stmt->execute(array(':username' => '(^|,)'.$username.'($|,)'));
1158 $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
1159 while ($row = array_shift($run)) {
1160 $data['is_catch_all'] = $row['address'];
1161 }
1162 return $data;
1163}
1164function is_valid_domain_name($domain_name) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001165 if (empty($domain_name)) {
1166 return false;
1167 }
1168 $domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46);
1169 return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
1170 && preg_match("/^.{1,253}$/", $domain_name)
1171 && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001172}
1173function set_tfa($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001174 global $pdo;
1175 global $yubi;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001176 global $tfa;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001177 $_data_log = $_data;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001178 $access_denied = null;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001179 !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
1180 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001181
1182 // check for empty user and role
1183 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
1184
1185 // check admin confirm password
1186 if ($access_denied === null) {
1187 $stmt = $pdo->prepare("SELECT `password` FROM `admin`
1188 WHERE `username` = :username");
1189 $stmt->execute(array(':username' => $username));
1190 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1191 if ($row) {
1192 if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
1193 else $access_denied = false;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001194 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001195 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001196
1197 // check mailbox confirm password
1198 if ($access_denied === null) {
1199 $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
1200 WHERE `username` = :username");
1201 $stmt->execute(array(':username' => $username));
1202 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1203 if ($row) {
1204 if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
1205 else $access_denied = false;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001206 }
1207 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001208
1209 // set access_denied error
1210 if ($access_denied){
1211 $_SESSION['return'][] = array(
1212 'type' => 'danger',
1213 'log' => array(__FUNCTION__, $_data_log),
1214 'msg' => 'access_denied'
1215 );
1216 return false;
1217 }
1218
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001219 switch ($_data["tfa_method"]) {
1220 case "yubi_otp":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001221 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1222 $yubico_id = $_data['yubico_id'];
1223 $yubico_key = $_data['yubico_key'];
1224 $yubi = new Auth_Yubico($yubico_id, $yubico_key);
1225 if (!$yubi) {
1226 $_SESSION['return'][] = array(
1227 'type' => 'danger',
1228 'log' => array(__FUNCTION__, $_data_log),
1229 'msg' => 'access_denied'
1230 );
1231 return false;
1232 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001233 if (!ctype_alnum($_data["otp_token"]) || strlen($_data["otp_token"]) != 44) {
1234 $_SESSION['return'][] = array(
1235 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001236 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001237 'msg' => 'tfa_token_invalid'
1238 );
1239 return false;
1240 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001241 $yauth = $yubi->verify($_data["otp_token"]);
1242 if (PEAR::isError($yauth)) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001243 $_SESSION['return'][] = array(
1244 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001245 'log' => array(__FUNCTION__, $_data_log),
1246 'msg' => array('yotp_verification_failed', $yauth->getMessage())
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001247 );
1248 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001249 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001250 try {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001251 // We could also do a modhex translation here
1252 $yubico_modhex_id = substr($_data["otp_token"], 0, 12);
1253 $stmt = $pdo->prepare("DELETE FROM `tfa`
1254 WHERE `username` = :username
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001255 AND (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001256 $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
1257 $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
1258 (:key_id, :username, 'yubi_otp', '1', :secret)");
1259 $stmt->execute(array(':key_id' => $key_id, ':username' => $username, ':secret' => $yubico_id . ':' . $yubico_key . ':' . $yubico_modhex_id));
1260 }
1261 catch (PDOException $e) {
1262 $_SESSION['return'][] = array(
1263 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001264 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001265 'msg' => array('mysql_error', $e)
1266 );
1267 return false;
1268 }
1269 $_SESSION['return'][] = array(
1270 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001271 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001272 'msg' => array('object_modified', htmlspecialchars($username))
1273 );
1274 break;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001275 case "totp":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001276 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1277 if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) {
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001278 //$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
1279 //$stmt->execute(array(':username' => $username));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001280 $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
1281 $stmt->execute(array($username, $key_id, $_POST['totp_secret']));
1282 $_SESSION['return'][] = array(
1283 'type' => 'success',
1284 'log' => array(__FUNCTION__, $_data_log),
1285 'msg' => array('object_modified', $username)
1286 );
1287 }
1288 else {
1289 $_SESSION['return'][] = array(
1290 'type' => 'danger',
1291 'log' => array(__FUNCTION__, $_data_log),
1292 'msg' => 'totp_verification_failed'
1293 );
1294 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001295 break;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001296 case "webauthn":
1297 $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
1298
1299 $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)
1300 VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
1301 $stmt->execute(array(
1302 $username,
1303 $key_id,
1304 base64_encode($_data['registration']->credentialId),
1305 $_data['registration']->credentialPublicKey,
1306 $_data['registration']->certificate,
1307 0
1308 ));
1309
1310 $_SESSION['return'][] = array(
1311 'type' => 'success',
1312 'log' => array(__FUNCTION__, $_data_log),
1313 'msg' => array('object_modified', $username)
1314 );
1315 break;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001316 case "none":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001317 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
1318 $stmt->execute(array(':username' => $username));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001319 $_SESSION['return'][] = array(
1320 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001321 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001322 'msg' => array('object_modified', htmlspecialchars($username))
1323 );
1324 break;
1325 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001326}
1327function fido2($_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001328 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001329 $_data_log = $_data;
1330 // Not logging registration data, only actions
1331 // Silent errors for "get" requests
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001332 switch ($_data["action"]) {
1333 case "register":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001334 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001335 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001336 $_SESSION['return'][] = array(
1337 'type' => 'danger',
1338 'log' => array(__FUNCTION__, $_data["action"]),
1339 'msg' => 'access_denied'
1340 );
1341 return false;
1342 }
1343 $stmt = $pdo->prepare("INSERT INTO `fido2` (`username`, `rpId`, `credentialPublicKey`, `certificateChain`, `certificate`, `certificateIssuer`, `certificateSubject`, `signatureCounter`, `AAGUID`, `credentialId`)
1344 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
1345 $stmt->execute(array(
1346 $username,
1347 $_data['registration']->rpId,
1348 $_data['registration']->credentialPublicKey,
1349 $_data['registration']->certificateChain,
1350 $_data['registration']->certificate,
1351 $_data['registration']->certificateIssuer,
1352 $_data['registration']->certificateSubject,
1353 $_data['registration']->signatureCounter,
1354 $_data['registration']->AAGUID,
1355 $_data['registration']->credentialId)
1356 );
1357 $_SESSION['return'][] = array(
1358 'type' => 'success',
1359 'log' => array(__FUNCTION__, $_data["action"]),
1360 'msg' => array('object_modified', $username)
1361 );
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001362 break;
1363 case "get_user_cids":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001364 // Used to exclude existing CredentialIds while registering
1365 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001366 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1367 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001368 }
1369 $stmt = $pdo->prepare("SELECT `credentialId` FROM `fido2` WHERE `username` = :username");
1370 $stmt->execute(array(':username' => $username));
1371 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1372 while($row = array_shift($rows)) {
1373 $cids[] = $row['credentialId'];
1374 }
1375 return $cids;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001376 break;
1377 case "get_all_cids":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001378 // Only needed when using fido2 with username
1379 $stmt = $pdo->query("SELECT `credentialId` FROM `fido2`");
1380 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1381 while($row = array_shift($rows)) {
1382 $cids[] = $row['credentialId'];
1383 }
1384 return $cids;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001385 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001386 case "get_by_b64cid":
1387 if (!isset($_data['cid']) || empty($_data['cid'])) {
1388 return false;
1389 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001390 $stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey`, SHA2(`credentialId`, 256) AS `cid` FROM `fido2` WHERE `credentialId` = :cid");
1391 $stmt->execute(array(':cid' => base64_decode($_data['cid'])));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001392 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1393 if (empty($row) || empty($row['credentialPublicKey']) || empty($row['username'])) {
1394 return false;
1395 }
1396 $data['pub_key'] = $row['credentialPublicKey'];
1397 $data['username'] = $row['username'];
1398 $data['subject'] = $row['certificateSubject'];
1399 $data['cid'] = $row['cid'];
1400 return $data;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001401 break;
1402 case "get_friendly_names":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001403 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001404 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1405 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001406 }
1407 $stmt = $pdo->prepare("SELECT SHA2(`credentialId`, 256) AS `cid`, `created`, `certificateSubject`, `friendlyName` FROM `fido2` WHERE `username` = :username");
1408 $stmt->execute(array(':username' => $username));
1409 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1410 while($row = array_shift($rows)) {
1411 $fns[] = array(
1412 "subject" => (empty($row['certificateSubject']) ? 'Unknown (' . $row['created'] . ')' : $row['certificateSubject']),
1413 "fn" => $row['friendlyName'],
1414 "cid" => $row['cid']
1415 );
1416 }
1417 return $fns;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001418 break;
1419 case "unset_fido2_key":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001420 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001421 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1422 $_SESSION['return'][] = array(
1423 'type' => 'danger',
1424 'log' => array(__FUNCTION__, $_data["action"]),
1425 'msg' => 'access_denied'
1426 );
1427 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001428 }
1429 $stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username AND SHA2(`credentialId`, 256) = :cid");
1430 $stmt->execute(array(
1431 ':username' => $username,
1432 ':cid' => $_data['post_data']['unset_fido2_key']
1433 ));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001434 $_SESSION['return'][] = array(
1435 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001436 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001437 'msg' => array('object_modified', htmlspecialchars($username))
1438 );
1439 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001440 case "edit_fn":
1441 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001442 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
1443 $_SESSION['return'][] = array(
1444 'type' => 'danger',
1445 'log' => array(__FUNCTION__, $_data["action"]),
1446 'msg' => 'access_denied'
1447 );
1448 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001449 }
1450 $stmt = $pdo->prepare("UPDATE `fido2` SET `friendlyName` = :friendlyName WHERE SHA2(`credentialId`, 256) = :cid AND `username` = :username");
1451 $stmt->execute(array(
1452 ':username' => $username,
1453 ':friendlyName' => $_data['fido2_attrs']['fido2_fn'],
1454 ':cid' => $_data['fido2_attrs']['fido2_cid']
1455 ));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001456 $_SESSION['return'][] = array(
1457 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001458 'log' => array(__FUNCTION__, $_data_log),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001459 'msg' => array('object_modified', htmlspecialchars($username))
1460 );
1461 break;
1462 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001463}
1464function unset_tfa_key($_data) {
1465 // Can only unset own keys
1466 // Needs at least one key left
1467 global $pdo;
1468 global $lang;
1469 $_data_log = $_data;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001470 $access_denied = null;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001471 $id = intval($_data['unset_tfa_key']);
1472 $username = $_SESSION['mailcow_cc_username'];
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001473
1474 // check for empty user and role
1475 if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
1476
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001477 try {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001478 if (!is_numeric($id)) $access_denied = true;
1479
1480 // set access_denied error
1481 if ($access_denied){
1482 $_SESSION['return'][] = array(
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001483 'type' => 'danger',
1484 'log' => array(__FUNCTION__, $_data_log),
1485 'msg' => 'access_denied'
1486 );
1487 return false;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001488 }
1489
1490 // check if it's last key
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001491 $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
1492 WHERE `username` = :username AND `active` = '1'");
1493 $stmt->execute(array(':username' => $username));
1494 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1495 if ($row['keys'] == "1") {
1496 $_SESSION['return'][] = array(
1497 'type' => 'danger',
1498 'log' => array(__FUNCTION__, $_data_log),
1499 'msg' => 'last_key'
1500 );
1501 return false;
1502 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001503
1504 // delete key
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001505 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
1506 $stmt->execute(array(':username' => $username, ':id' => $id));
1507 $_SESSION['return'][] = array(
1508 'type' => 'success',
1509 'log' => array(__FUNCTION__, $_data_log),
1510 'msg' => array('object_modified', $username)
1511 );
1512 }
1513 catch (PDOException $e) {
1514 $_SESSION['return'][] = array(
1515 'type' => 'danger',
1516 'log' => array(__FUNCTION__, $_data_log),
1517 'msg' => array('mysql_error', $e)
1518 );
1519 return false;
1520 }
1521}
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001522function get_tfa($username = null, $id = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001523 global $pdo;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001524 if (isset($_SESSION['mailcow_cc_username'])) {
1525 $username = $_SESSION['mailcow_cc_username'];
1526 }
1527 elseif (empty($username)) {
1528 return false;
1529 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001530
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001531 if (!isset($id)){
1532 // fetch all tfa methods - just get information about possible authenticators
1533 $stmt = $pdo->prepare("SELECT `id`, `key_id`, `authmech` FROM `tfa`
1534 WHERE `username` = :username AND `active` = '1'");
1535 $stmt->execute(array(':username' => $username));
1536 $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
1537
1538 // no tfa methods found
1539 if (count($results) == 0) {
1540 $data['name'] = 'none';
1541 $data['pretty'] = "-";
1542 $data['additional'] = array();
1543 return $data;
1544 }
1545
1546 $data['additional'] = $results;
1547 return $data;
1548 } else {
1549 // fetch specific authenticator details by id
1550 $stmt = $pdo->prepare("SELECT * FROM `tfa`
1551 WHERE `username` = :username AND `id` = :id AND `active` = '1'");
1552 $stmt->execute(array(':username' => $username, ':id' => $id));
1553 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1554
1555 if (isset($row["authmech"])) {
1556 switch ($row["authmech"]) {
1557 case "yubi_otp":
1558 $data['name'] = "yubi_otp";
1559 $data['pretty'] = "Yubico OTP";
1560 $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username AND `id` = :id");
1561 $stmt->execute(array(
1562 ':username' => $username,
1563 ':id' => $id
1564 ));
1565 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1566 while($row = array_shift($rows)) {
1567 $data['additional'][] = $row;
1568 }
1569 return $data;
1570 break;
1571 // u2f - deprecated, should be removed
1572 case "u2f":
1573 $data['name'] = "u2f";
1574 $data['pretty'] = "Fido U2F";
1575 $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username AND `id` = :id");
1576 $stmt->execute(array(
1577 ':username' => $username,
1578 ':id' => $id
1579 ));
1580 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1581 while($row = array_shift($rows)) {
1582 $data['additional'][] = $row;
1583 }
1584 return $data;
1585 break;
1586 case "hotp":
1587 $data['name'] = "hotp";
1588 $data['pretty'] = "HMAC-based OTP";
1589 return $data;
1590 break;
1591 case "totp":
1592 $data['name'] = "totp";
1593 $data['pretty'] = "Time-based OTP";
1594 $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username AND `id` = :id");
1595 $stmt->execute(array(
1596 ':username' => $username,
1597 ':id' => $id
1598 ));
1599 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1600 while($row = array_shift($rows)) {
1601 $data['additional'][] = $row;
1602 }
1603 return $data;
1604 break;
1605 case "webauthn":
1606 $data['name'] = "webauthn";
1607 $data['pretty'] = "WebAuthn";
1608 $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username AND `id` = :id");
1609 $stmt->execute(array(
1610 ':username' => $username,
1611 ':id' => $id
1612 ));
1613 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1614 while($row = array_shift($rows)) {
1615 $data['additional'][] = $row;
1616 }
1617 return $data;
1618 break;
1619 default:
1620 $data['name'] = 'none';
1621 $data['pretty'] = "-";
1622 return $data;
1623 break;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001624 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001625 }
1626 else {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001627 $data['name'] = 'none';
1628 $data['pretty'] = "-";
1629 return $data;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001630 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001631 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001632}
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001633function verify_tfa_login($username, $_data) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001634 global $pdo;
1635 global $yubi;
1636 global $u2f;
1637 global $tfa;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001638 global $WebAuthn;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001639
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001640 if ($_data['tfa_method'] != 'u2f'){
1641
1642 switch ($_data["tfa_method"]) {
1643 case "yubi_otp":
1644 if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) {
1645 $_SESSION['return'][] = array(
1646 'type' => 'danger',
1647 'log' => array(__FUNCTION__, $username, '*'),
1648 'msg' => array('yotp_verification_failed', 'token length error')
1649 );
1650 return false;
1651 }
1652 $yubico_modhex_id = substr($_data['token'], 0, 12);
1653 $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
1654 WHERE `username` = :username
1655 AND `authmech` = 'yubi_otp'
1656 AND `active` = '1'
1657 AND `secret` LIKE :modhex");
1658 $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
1659 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1660 $yubico_auth = explode(':', $row['secret']);
1661 $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
1662 $yauth = $yubi->verify($_data['token']);
1663 if (PEAR::isError($yauth)) {
1664 $_SESSION['return'][] = array(
1665 'type' => 'danger',
1666 'log' => array(__FUNCTION__, $username, '*'),
1667 'msg' => array('yotp_verification_failed', $yauth->getMessage())
1668 );
1669 return false;
1670 }
1671 else {
1672 $_SESSION['tfa_id'] = $row['id'];
1673 $_SESSION['return'][] = array(
1674 'type' => 'success',
1675 'log' => array(__FUNCTION__, $username, '*'),
1676 'msg' => 'verified_yotp_login'
1677 );
1678 return true;
1679 }
1680 $_SESSION['return'][] = array(
1681 'type' => 'danger',
1682 'log' => array(__FUNCTION__, $username, '*'),
1683 'msg' => array('yotp_verification_failed', 'unknown')
1684 );
1685 return false;
1686 break;
1687 case "hotp":
1688 return false;
1689 break;
1690 case "totp":
1691 try {
1692 $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
1693 WHERE `username` = :username
1694 AND `authmech` = 'totp'
1695 AND `id` = :id
1696 AND `active`='1'");
1697 $stmt->execute(array(':username' => $username, ':id' => $_data['id']));
1698 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1699 foreach ($rows as $row) {
1700 if ($tfa->verifyCode($row['secret'], $_data['token']) === true) {
1701 $_SESSION['tfa_id'] = $row['id'];
1702 $_SESSION['return'][] = array(
1703 'type' => 'success',
1704 'log' => array(__FUNCTION__, $username, '*'),
1705 'msg' => 'verified_totp_login'
1706 );
1707 return true;
1708 }
1709 }
1710 $_SESSION['return'][] = array(
1711 'type' => 'danger',
1712 'log' => array(__FUNCTION__, $username, '*'),
1713 'msg' => 'totp_verification_failed'
1714 );
1715 return false;
1716 }
1717 catch (PDOException $e) {
1718 $_SESSION['return'][] = array(
1719 'type' => 'danger',
1720 'log' => array(__FUNCTION__, $username, '*'),
1721 'msg' => array('mysql_error', $e)
1722 );
1723 return false;
1724 }
1725 break;
1726 case "webauthn":
1727 $tokenData = json_decode($_data['token']);
1728 $clientDataJSON = base64_decode($tokenData->clientDataJSON);
1729 $authenticatorData = base64_decode($tokenData->authenticatorData);
1730 $signature = base64_decode($tokenData->signature);
1731 $id = base64_decode($tokenData->id);
1732 $challenge = $_SESSION['challenge'];
1733
1734 $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id AND `active`='1'");
1735 $stmt->execute(array(':id' => $_data['id']));
1736 $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC);
1737
1738 if (empty($process_webauthn)){
1739 $_SESSION['return'][] = array(
1740 'type' => 'danger',
1741 'log' => array(__FUNCTION__, $username, '*'),
1742 'msg' => array('webauthn_verification_failed', 'authenticator not found')
1743 );
1744 return false;
1745 }
1746
1747 if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) {
1748 $_SESSION['return'][] = array(
1749 'type' => 'danger',
1750 'log' => array(__FUNCTION__, $username, '*'),
1751 'msg' => array('webauthn_verification_failed', 'publicKey not found')
1752 );
1753 return false;
1754 }
1755
1756 try {
1757 $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
1758 }
1759 catch (Throwable $ex) {
1760 $_SESSION['return'][] = array(
1761 'type' => 'danger',
1762 'log' => array(__FUNCTION__, $username, '*'),
1763 'msg' => array('webauthn_verification_failed', $ex->getMessage())
1764 );
1765 return false;
1766 }
1767
1768 $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
1769 $stmt->execute(array(':username' => $process_webauthn['username']));
1770 $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
1771 if ($obj_props['superadmin'] === 1) {
1772 $_SESSION["mailcow_cc_role"] = "admin";
1773 }
1774 elseif ($obj_props['superadmin'] === 0) {
1775 $_SESSION["mailcow_cc_role"] = "domainadmin";
1776 }
1777 else {
1778 $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
1779 $stmt->execute(array(':username' => $process_webauthn['username']));
1780 $row = $stmt->fetch(PDO::FETCH_ASSOC);
1781 if (!empty($row['username'])) {
1782 $_SESSION["mailcow_cc_role"] = "user";
1783 } else {
1784 $_SESSION['return'][] = array(
1785 'type' => 'danger',
1786 'log' => array(__FUNCTION__, $username, '*'),
1787 'msg' => array('webauthn_verification_failed', 'could not determine user role')
1788 );
1789 return false;
1790 }
1791 }
1792
1793 if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
1794 $_SESSION['return'][] = array(
1795 'type' => 'danger',
1796 'log' => array(__FUNCTION__, $username, '*'),
1797 'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry')
1798 );
1799 return false;
1800 }
1801
1802 $_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
1803 $_SESSION['tfa_id'] = $process_webauthn['id'];
1804 $_SESSION['authReq'] = null;
1805 unset($_SESSION["challenge"]);
1806 $_SESSION['return'][] = array(
1807 'type' => 'success',
1808 'log' => array("webauthn_login"),
1809 'msg' => array('logged_in_as', $process_webauthn['username'])
1810 );
1811 return true;
1812 break;
1813 default:
1814 $_SESSION['return'][] = array(
1815 'type' => 'danger',
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001816 'log' => array(__FUNCTION__, $username, '*'),
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001817 'msg' => 'unknown_tfa_method'
1818 );
1819 return false;
1820 break;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001821 }
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001822
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001823 return false;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001824 } else {
1825 // delete old keys that used u2f
1826 $stmt = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
1827 $stmt->execute(array(':username' => $username));
1828 $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
1829 if (count($rows) == 0) return false;
1830
1831 $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
1832 $stmt->execute(array(':username' => $username));
1833 return true;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001834 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001835}
1836function admin_api($access, $action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001837 global $pdo;
1838 if ($_SESSION['mailcow_cc_role'] != "admin") {
1839 $_SESSION['return'][] = array(
1840 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001841 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001842 'msg' => 'access_denied'
1843 );
1844 return false;
1845 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001846 if ($access !== "ro" && $access !== "rw") {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001847 $_SESSION['return'][] = array(
1848 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001849 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001850 'msg' => 'invalid access type'
1851 );
1852 return false;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001853 }
1854 if ($action == "edit") {
1855 $active = (!empty($data['active'])) ? 1 : 0;
1856 $skip_ip_check = (isset($data['skip_ip_check'])) ? 1 : 0;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001857 $allow_from = array();
1858 if (isset($data['allow_from'])) {
1859 $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from']));
1860 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001861 foreach ($allow_from as $key => $val) {
1862 if (empty($val)) {
1863 unset($allow_from[$key]);
1864 continue;
1865 }
1866 if (valid_network($val) !== true) {
1867 $_SESSION['return'][] = array(
1868 'type' => 'warning',
1869 'log' => array(__FUNCTION__, $data),
1870 'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key]))
1871 );
1872 unset($allow_from[$key]);
1873 continue;
1874 }
1875 }
1876 $allow_from = implode(',', array_unique(array_filter($allow_from)));
1877 if (empty($allow_from) && $skip_ip_check == 0) {
1878 $_SESSION['return'][] = array(
1879 'type' => 'danger',
1880 'log' => array(__FUNCTION__, $data),
1881 'msg' => 'ip_list_empty'
1882 );
1883 return false;
1884 }
1885 $api_key = implode('-', array(
1886 strtoupper(bin2hex(random_bytes(3))),
1887 strtoupper(bin2hex(random_bytes(3))),
1888 strtoupper(bin2hex(random_bytes(3))),
1889 strtoupper(bin2hex(random_bytes(3))),
1890 strtoupper(bin2hex(random_bytes(3)))
1891 ));
1892 $stmt = $pdo->query("SELECT `api_key` FROM `api` WHERE `access` = '" . $access . "'");
1893 $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
1894 if (empty($num_results)) {
1895 $stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `skip_ip_check`, `active`, `allow_from`, `access`)
1896 VALUES (:api_key, :skip_ip_check, :active, :allow_from, :access);");
1897 $stmt->execute(array(
1898 ':api_key' => $api_key,
1899 ':skip_ip_check' => $skip_ip_check,
1900 ':active' => $active,
1901 ':allow_from' => $allow_from,
1902 ':access' => $access
1903 ));
1904 }
1905 else {
1906 if ($skip_ip_check == 0) {
1907 $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
1908 `active` = :active,
1909 `allow_from` = :allow_from
1910 WHERE `access` = :access;");
1911 $stmt->execute(array(
1912 ':active' => $active,
1913 ':skip_ip_check' => $skip_ip_check,
1914 ':allow_from' => $allow_from,
1915 ':access' => $access
1916 ));
1917 }
1918 else {
1919 $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
1920 `active` = :active
1921 WHERE `access` = :access;");
1922 $stmt->execute(array(
1923 ':active' => $active,
1924 ':skip_ip_check' => $skip_ip_check,
1925 ':access' => $access
1926 ));
1927 }
1928 }
1929 }
1930 elseif ($action == "regen_key") {
1931 $api_key = implode('-', array(
1932 strtoupper(bin2hex(random_bytes(3))),
1933 strtoupper(bin2hex(random_bytes(3))),
1934 strtoupper(bin2hex(random_bytes(3))),
1935 strtoupper(bin2hex(random_bytes(3))),
1936 strtoupper(bin2hex(random_bytes(3)))
1937 ));
1938 $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `access` = :access");
1939 $stmt->execute(array(
1940 ':api_key' => $api_key,
1941 ':access' => $access
1942 ));
1943 }
1944 elseif ($action == "get") {
1945 $stmt = $pdo->query("SELECT * FROM `api` WHERE `access` = '" . $access . "'");
1946 $apidata = $stmt->fetch(PDO::FETCH_ASSOC);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001947 if ($apidata !== false) {
1948 $apidata['allow_from'] = str_replace(',', PHP_EOL, $apidata['allow_from']);
1949 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001950 return $apidata;
1951 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001952 $_SESSION['return'][] = array(
1953 'type' => 'success',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001954 'log' => array(__FUNCTION__, $data),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001955 'msg' => 'admin_api_modified'
1956 );
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001957}
1958function license($action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001959 global $pdo;
1960 global $redis;
1961 global $lang;
1962 if ($_SESSION['mailcow_cc_role'] != "admin") {
1963 $_SESSION['return'][] = array(
1964 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001965 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001966 'msg' => 'access_denied'
1967 );
1968 return false;
1969 }
1970 switch ($action) {
1971 case "verify":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001972 // Keep result until revalidate button is pressed or session expired
1973 $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
1974 $versions = $stmt->fetch(PDO::FETCH_ASSOC);
1975 $post = array('guid' => $versions['version']);
1976 $curl = curl_init('https://verify.mailcow.email');
1977 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
1978 curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
1979 curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
1980 $response = curl_exec($curl);
1981 curl_close($curl);
1982 $json_return = json_decode($response, true);
1983 if ($response && $json_return) {
1984 if ($json_return['response'] === "ok") {
1985 $_SESSION['gal']['valid'] = "true";
1986 $_SESSION['gal']['c'] = $json_return['c'];
1987 $_SESSION['gal']['s'] = $json_return['s'];
1988 if ($json_return['m'] == 'NoMoore') {
1989 $_SESSION['gal']['m'] = '🐄';
1990 }
1991 else {
1992 $_SESSION['gal']['m'] = str_repeat('🐄', substr_count($json_return['m'], 'o'));
1993 }
1994 }
1995 elseif ($json_return['response'] === "invalid") {
1996 $_SESSION['gal']['valid'] = "false";
1997 $_SESSION['gal']['c'] = $lang['mailbox']['no'];
1998 $_SESSION['gal']['s'] = $lang['mailbox']['no'];
1999 $_SESSION['gal']['m'] = $lang['mailbox']['no'];
2000 }
2001 }
2002 else {
2003 $_SESSION['gal']['valid'] = "false";
2004 $_SESSION['gal']['c'] = $lang['danger']['temp_error'];
2005 $_SESSION['gal']['s'] = $lang['danger']['temp_error'];
2006 $_SESSION['gal']['m'] = $lang['danger']['temp_error'];
2007 }
2008 try {
2009 // json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1
2010 $redis->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal']));
2011 }
2012 catch (RedisException $e) {
2013 $_SESSION['return'][] = array(
2014 'type' => 'danger',
2015 'log' => array(__FUNCTION__, $_action, $_data_log),
2016 'msg' => array('redis_error', $e)
2017 );
2018 return false;
2019 }
2020 return $_SESSION['gal']['valid'];
2021 break;
2022 case "guid":
2023 $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
2024 $versions = $stmt->fetch(PDO::FETCH_ASSOC);
2025 return $versions['version'];
2026 break;
2027 }
2028}
2029function rspamd_ui($action, $data = null) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002030 if ($_SESSION['mailcow_cc_role'] != "admin") {
2031 $_SESSION['return'][] = array(
2032 'type' => 'danger',
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002033 'log' => array(__FUNCTION__),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002034 'msg' => 'access_denied'
2035 );
2036 return false;
2037 }
2038 switch ($action) {
2039 case "edit":
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002040 $rspamd_ui_pass = $data['rspamd_ui_pass'];
2041 $rspamd_ui_pass2 = $data['rspamd_ui_pass2'];
2042 if (empty($rspamd_ui_pass) || empty($rspamd_ui_pass2)) {
2043 $_SESSION['return'][] = array(
2044 'type' => 'danger',
2045 'log' => array(__FUNCTION__, '*', '*'),
2046 'msg' => 'password_empty'
2047 );
2048 return false;
2049 }
2050 if ($rspamd_ui_pass != $rspamd_ui_pass2) {
2051 $_SESSION['return'][] = array(
2052 'type' => 'danger',
2053 'log' => array(__FUNCTION__, '*', '*'),
2054 'msg' => 'password_mismatch'
2055 );
2056 return false;
2057 }
2058 if (strlen($rspamd_ui_pass) < 6) {
2059 $_SESSION['return'][] = array(
2060 'type' => 'danger',
2061 'log' => array(__FUNCTION__, '*', '*'),
2062 'msg' => 'rspamd_ui_pw_length'
2063 );
2064 return false;
2065 }
2066 $docker_return = docker('post', 'rspamd-mailcow', 'exec', array('cmd' => 'rspamd', 'task' => 'worker_password', 'raw' => $rspamd_ui_pass), array('Content-Type: application/json'));
2067 if ($docker_return_array = json_decode($docker_return, true)) {
2068 if ($docker_return_array['type'] == 'success') {
2069 $_SESSION['return'][] = array(
2070 'type' => 'success',
2071 'log' => array(__FUNCTION__, '*', '*'),
2072 'msg' => 'rspamd_ui_pw_set'
2073 );
2074 return true;
2075 }
2076 else {
2077 $_SESSION['return'][] = array(
2078 'type' => $docker_return_array['type'],
2079 'log' => array(__FUNCTION__, '*', '*'),
2080 'msg' => $docker_return_array['msg']
2081 );
2082 return false;
2083 }
2084 }
2085 else {
2086 $_SESSION['return'][] = array(
2087 'type' => 'danger',
2088 'log' => array(__FUNCTION__, '*', '*'),
2089 'msg' => 'unknown'
2090 );
2091 return false;
2092 }
2093 break;
2094 }
2095}
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01002096
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002097function get_logs($application, $lines = false) {
2098 if ($lines === false) {
2099 $lines = $GLOBALS['LOG_LINES'] - 1;
2100 }
2101 elseif(is_numeric($lines) && $lines >= 1) {
2102 $lines = abs(intval($lines) - 1);
2103 }
2104 else {
2105 list ($from, $to) = explode('-', $lines);
2106 $from = intval($from);
2107 $to = intval($to);
2108 if ($from < 1 || $to < $from) { return false; }
2109 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002110 global $redis;
2111 global $pdo;
2112 if ($_SESSION['mailcow_cc_role'] != "admin") {
2113 return false;
2114 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002115 // SQL
2116 if ($application == "mailcow-ui") {
2117 if (isset($from) && isset($to)) {
2118 $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :from, :to");
2119 $stmt->execute(array(
2120 ':from' => $from - 1,
2121 ':to' => $to
2122 ));
2123 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
2124 }
2125 else {
2126 $stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :lines");
2127 $stmt->execute(array(
2128 ':lines' => $lines + 1,
2129 ));
2130 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
2131 }
2132 if (is_array($data)) {
2133 return $data;
2134 }
2135 }
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002136 if ($application == "sasl") {
2137 if (isset($from) && isset($to)) {
2138 $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :from, :to");
2139 $stmt->execute(array(
2140 ':from' => $from - 1,
2141 ':to' => $to
2142 ));
2143 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
2144 }
2145 else {
2146 $stmt = $pdo->prepare("SELECT * FROM `sasl_log` ORDER BY `datetime` DESC LIMIT :lines");
2147 $stmt->execute(array(
2148 ':lines' => $lines + 1,
2149 ));
2150 $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
2151 }
2152 if (is_array($data)) {
2153 return $data;
2154 }
2155 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01002156 // Redis
2157 if ($application == "dovecot-mailcow") {
2158 if (isset($from) && isset($to)) {
2159 $data = $redis->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1);
2160 }
2161 else {
2162 $data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines);
2163 }
2164 if ($data) {
2165 foreach ($data as $json_line) {
2166 $data_array[] = json_decode($json_line, true);
2167 }
2168 return $data_array;
2169 }
2170 }
2171 if ($application == "postfix-mailcow") {
2172 if (isset($from) && isset($to)) {
2173 $data = $redis->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1);
2174 }
2175 else {
2176 $data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines);
2177 }
2178 if ($data) {
2179 foreach ($data as $json_line) {
2180 $data_array[] = json_decode($json_line, true);
2181 }
2182 return $data_array;
2183 }
2184 }
2185 if ($application == "sogo-mailcow") {
2186 if (isset($from) && isset($to)) {
2187 $data = $redis->lRange('SOGO_LOG', $from - 1, $to - 1);
2188 }
2189 else {
2190 $data = $redis->lRange('SOGO_LOG', 0, $lines);
2191 }
2192 if ($data) {
2193 foreach ($data as $json_line) {
2194 $data_array[] = json_decode($json_line, true);
2195 }
2196 return $data_array;
2197 }
2198 }
2199 if ($application == "watchdog-mailcow") {
2200 if (isset($from) && isset($to)) {
2201 $data = $redis->lRange('WATCHDOG_LOG', $from - 1, $to - 1);
2202 }
2203 else {
2204 $data = $redis->lRange('WATCHDOG_LOG', 0, $lines);
2205 }
2206 if ($data) {
2207 foreach ($data as $json_line) {
2208 $data_array[] = json_decode($json_line, true);
2209 }
2210 return $data_array;
2211 }
2212 }
2213 if ($application == "acme-mailcow") {
2214 if (isset($from) && isset($to)) {
2215 $data = $redis->lRange('ACME_LOG', $from - 1, $to - 1);
2216 }
2217 else {
2218 $data = $redis->lRange('ACME_LOG', 0, $lines);
2219 }
2220 if ($data) {
2221 foreach ($data as $json_line) {
2222 $data_array[] = json_decode($json_line, true);
2223 }
2224 return $data_array;
2225 }
2226 }
2227 if ($application == "ratelimited") {
2228 if (isset($from) && isset($to)) {
2229 $data = $redis->lRange('RL_LOG', $from - 1, $to - 1);
2230 }
2231 else {
2232 $data = $redis->lRange('RL_LOG', 0, $lines);
2233 }
2234 if ($data) {
2235 foreach ($data as $json_line) {
2236 $data_array[] = json_decode($json_line, true);
2237 }
2238 return $data_array;
2239 }
2240 }
2241 if ($application == "api-mailcow") {
2242 if (isset($from) && isset($to)) {
2243 $data = $redis->lRange('API_LOG', $from - 1, $to - 1);
2244 }
2245 else {
2246 $data = $redis->lRange('API_LOG', 0, $lines);
2247 }
2248 if ($data) {
2249 foreach ($data as $json_line) {
2250 $data_array[] = json_decode($json_line, true);
2251 }
2252 return $data_array;
2253 }
2254 }
2255 if ($application == "netfilter-mailcow") {
2256 if (isset($from) && isset($to)) {
2257 $data = $redis->lRange('NETFILTER_LOG', $from - 1, $to - 1);
2258 }
2259 else {
2260 $data = $redis->lRange('NETFILTER_LOG', 0, $lines);
2261 }
2262 if ($data) {
2263 foreach ($data as $json_line) {
2264 $data_array[] = json_decode($json_line, true);
2265 }
2266 return $data_array;
2267 }
2268 }
2269 if ($application == "autodiscover-mailcow") {
2270 if (isset($from) && isset($to)) {
2271 $data = $redis->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1);
2272 }
2273 else {
2274 $data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines);
2275 }
2276 if ($data) {
2277 foreach ($data as $json_line) {
2278 $data_array[] = json_decode($json_line, true);
2279 }
2280 return $data_array;
2281 }
2282 }
2283 if ($application == "rspamd-history") {
2284 $curl = curl_init();
2285 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
2286 if (!is_numeric($lines)) {
2287 list ($from, $to) = explode('-', $lines);
2288 curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?from=" . intval($from) . "&to=" . intval($to));
2289 }
2290 else {
2291 curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?to=" . intval($lines));
2292 }
2293 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
2294 $history = curl_exec($curl);
2295 if (!curl_errno($curl)) {
2296 $data_array = json_decode($history, true);
2297 curl_close($curl);
2298 return $data_array['rows'];
2299 }
2300 curl_close($curl);
2301 return false;
2302 }
2303 if ($application == "rspamd-stats") {
2304 $curl = curl_init();
2305 curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
2306 curl_setopt($curl, CURLOPT_URL,"http://rspamd/stat");
2307 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
2308 $stats = curl_exec($curl);
2309 if (!curl_errno($curl)) {
2310 $data_array = json_decode($stats, true);
2311 curl_close($curl);
2312 return $data_array;
2313 }
2314 curl_close($curl);
2315 return false;
2316 }
2317 return false;
2318}
2319function getGUID() {
2320 if (function_exists('com_create_guid')) {
2321 return com_create_guid();
2322 }
2323 mt_srand((double)microtime()*10000);//optional for php 4.2.0 and up.
2324 $charid = strtoupper(md5(uniqid(rand(), true)));
2325 $hyphen = chr(45);// "-"
2326 return substr($charid, 0, 8).$hyphen
2327 .substr($charid, 8, 4).$hyphen
2328 .substr($charid,12, 4).$hyphen
2329 .substr($charid,16, 4).$hyphen
2330 .substr($charid,20,12);
2331}
2332function solr_status() {
2333 $curl = curl_init();
2334 $endpoint = 'http://solr:8983/solr/admin/cores';
2335 $params = array(
2336 'action' => 'STATUS',
2337 'core' => 'dovecot-fts',
2338 'indexInfo' => 'true'
2339 );
2340 $url = $endpoint . '?' . http_build_query($params);
2341 curl_setopt($curl, CURLOPT_URL, $url);
2342 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
2343 curl_setopt($curl, CURLOPT_POST, 0);
2344 curl_setopt($curl, CURLOPT_TIMEOUT, 10);
2345 $response_core = curl_exec($curl);
2346 if ($response_core === false) {
2347 $err = curl_error($curl);
2348 curl_close($curl);
2349 return false;
2350 }
2351 else {
2352 curl_close($curl);
2353 $curl = curl_init();
2354 $status_core = json_decode($response_core, true);
2355 $url = 'http://solr:8983/solr/admin/info/system';
2356 curl_setopt($curl, CURLOPT_URL, $url);
2357 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
2358 curl_setopt($curl, CURLOPT_POST, 0);
2359 curl_setopt($curl, CURLOPT_TIMEOUT, 10);
2360 $response_sysinfo = curl_exec($curl);
2361 if ($response_sysinfo === false) {
2362 $err = curl_error($curl);
2363 curl_close($curl);
2364 return false;
2365 }
2366 else {
2367 curl_close($curl);
2368 $status_sysinfo = json_decode($response_sysinfo, true);
2369 $status = array_merge($status_core, $status_sysinfo);
2370 return (!empty($status['status']['dovecot-fts']) && !empty($status['jvm']['memory'])) ? $status : false;
2371 }
2372 return (!empty($status['status']['dovecot-fts'])) ? $status['status']['dovecot-fts'] : false;
2373 }
2374 return false;
2375}
2376
2377function cleanupJS($ignore = '', $folder = '/tmp/*.js') {
2378 $now = time();
2379 foreach (glob($folder) as $filename) {
2380 if(strpos($filename, $ignore) !== false) {
2381 continue;
2382 }
2383 if (is_file($filename)) {
2384 if ($now - filemtime($filename) >= 60 * 60) {
2385 unlink($filename);
2386 }
2387 }
2388 }
2389}
2390
2391function cleanupCSS($ignore = '', $folder = '/tmp/*.css') {
2392 $now = time();
2393 foreach (glob($folder) as $filename) {
2394 if(strpos($filename, $ignore) !== false) {
2395 continue;
2396 }
2397 if (is_file($filename)) {
2398 if ($now - filemtime($filename) >= 60 * 60) {
2399 unlink($filename);
2400 }
2401 }
2402 }
2403}
2404
2405?>