diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 8587219f..00000000 --- a/.drone.yml +++ /dev/null @@ -1,120 +0,0 @@ ---- -kind: pipeline -name: integration-testing - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: prepare-tests - pull: default - image: timovibritannia/ansible - commands: - - git clone https://github.com/mailcow/mailcow-integration-tests.git --branch $(curl -sL https://api.github.com/repos/mailcow/mailcow-integration-tests/releases/latest | jq -r '.tag_name') --single-branch . - - chmod +x ci.sh - - chmod +x ci-ssh.sh - - chmod +x ci-piprequierments.sh - - ./ci.sh - - wget -O group_vars/all/secrets.yml $SECRETS_DOWNLOAD_URL --quiet - environment: - SECRETS_DOWNLOAD_URL: - from_secret: SECRETS_DOWNLOAD_URL - VAULT_PW: - from_secret: VAULT_PW - when: - branch: - - master - - staging - event: - - push - -- name: lint - pull: default - image: timovibritannia/ansible - commands: - - ansible-lint ./ - when: - branch: - - master - - staging - event: - - push - -- name: create-server - pull: default - image: timovibritannia/ansible - commands: - - ./ci-piprequierments.sh - - ansible-playbook mailcow-start-server.yml --diff - - ./ci-ssh.sh - environment: - ANSIBLE_HOST_KEY_CHECKING: false - ANSIBLE_FORCE_COLOR: true - when: - branch: - - master - - staging - event: - - push - -- name: setup-server - pull: default - image: timovibritannia/ansible - commands: - - sleep 120 - - ./ci-piprequierments.sh - - ansible-playbook mailcow-setup-server.yml --private-key /drone/src/id_ssh_rsa --diff - environment: - ANSIBLE_HOST_KEY_CHECKING: false - ANSIBLE_FORCE_COLOR: true - when: - branch: - - master - - staging - event: - - push - -- name: run-tests - pull: default - image: timovibritannia/ansible - commands: - - ./ci-piprequierments.sh - - ansible-playbook mailcow-integration-tests.yml --private-key /drone/src/id_ssh_rsa --diff - environment: - ANSIBLE_HOST_KEY_CHECKING: false - ANSIBLE_FORCE_COLOR: true - when: - branch: - - master - - staging - event: - - push - -- name: delete-server - pull: default - image: timovibritannia/ansible - commands: - - ./ci-piprequierments.sh - - ansible-playbook mailcow-delete-server.yml --diff - environment: - ANSIBLE_HOST_KEY_CHECKING: false - ANSIBLE_FORCE_COLOR: true - when: - branch: - - master - - staging - event: - - push - status: - - failure - - success - ---- -kind: signature -hmac: f6619243fe2a27563291c9f2a46d93ffbc3b6dced9a05f23e64b555ce03a31e5 - -... diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 84357090..00000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -sudo: required -services: -- docker -script: -- echo 'Europe/Berlin' | MAILCOW_HOSTNAME=build.mailcow ./generate_config.sh -- docker-compose pull --ignore-pull-failures --parallel -- docker-compose build -- docker login --username=$DOCKER_HUB_USERNAME --password=$DOCKER_HUB_PASSWORD -- docker-compose push -branches: - only: - - master_disabled -env: - global: - - secure: MpxpTwD7f0CNEVLitSpVmocK7O9r+BwFE1deEHK4AlQo/oc9cOlhGe1EL3mx9zbglPmjlDg/8kMUGv6vSirIabfBo9Szjps76bHckFr9lr2Ykkg0e29oC8pgPpSXD1eY/1ZIN/FvIkxpUFLETo1okS/j9q/A0DCGFmti0n3EoMORsgRz9CpNAiEh0zpSd6+euPAGHuczuCrDuO84my9bIOCjA/+aPunHNeXiuM8yIM2SxCSyGtIKT0+jvquIvLF58VxivysXBlRfhDn8fhB09nXA2Ru/derYQACfcmNSn9Pd4bDpebPJW5B9H/XA8xjb58uKinUlncbAMB/QnxoT75j9YRWJZRSQ+34XNYP6ZgK9soZ2TC6djQyEKTUu45Kp/1s+poSn42m9jytJJTmmK0KxsZTRcC8JD5nrjIMZWPUNNTwC5L4+I7ZRWg2WooK3LNyq1Ng8Hn6W77wSgsvAJw2HD3Lx58AprGUhHuBeaIZRuSN9aKwZrl9vKQJLqPnOp/nF2EC6kot5HYYtcotGtETXPUDih21gWD5ZM2BqVqYfQQnJnNMgeYmMdj6QQuTFqhuNJf7hXRIRkTnD3j1gDOLKQZazW0+N2JE8XWDFwi6fKScDsxT85lJti9HmzHa7+k4RVHmUYuDgRoPuzUgjWHvPsiz3/Z8WQ9JYpH84S8w= - - secure: fWzZisT6nGDNL4lf6tXB07eFG2drgBakHxzdF/NFVvzuP861RFR6omuL+ED0PgXrEHDJBxaBLv52je8irmUXrAH1CNr7T8DWiZo/h5h609Uzr+38T1NnIu4krL0Wo6/CDwlLKnzqTq9yBIZLQSHVJmo8AOpo1JPIi2ajodqj9ZfmAxDQTQl+G6zvQjtqIkYHsHY7A44Rto0f14ykn7w2S82Jn6Ry89VNI5V1WEO3sMpM/XekNP/HokNcRIuntL/0+kuLvTJ5akGoTjBQxSnSW95opzPeGky74HRU2obExJYqKvF0VfVJRNAqejwjIiFIbbjqV0Sk5391kFuhuBErQQDM1bOHGdxZ41HsJH29qNWIl7C33Yl10qERoqecgsJ1N/bS2ZEmWqm/zQh5GClCXPvYmzEqMYsMGM3vjbKdjDlc1Wh2w/eFclsXN9LSXh1mc35rtj46frcT6e5Kof87AIfC9hTgDvk9kAsyjaHMkSHSZthbZXCIcsD8qriNm5UqfFBYD79mPIP1S2YMQ2jscCsjHOZgYVrcm0kzDF21J1w6H0Lo7d1jw37LYlegBdtLQ9gYgqY2D5m+nxWuVoD5FZmpR+5JGtK+ootyLFF8aiFoHXd4op1JCxRLjgkmnZKXzw3kTQSpE7oa7CgzchtQmK2nqcqla1b5Qk7ilVcjooo= diff --git a/README.md b/README.md index 7abd1024..7f7907d2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## We stand with 🇺🇦 -[![master build status](https://img.shields.io/drone/build/mailcow/mailcow-dockerized/master?label=master%20build&server=https%3A%2F%2Fdrone.mailcow.email)](https://drone.mailcow.email/mailcow/mailcow-dockerized) [![staging build status](https://img.shields.io/drone/build/mailcow/mailcow-dockerized/staging?label=staging%20build&server=https%3A%2F%2Fdrone.mailcow.email)](https://drone.mailcow.email/mailcow/mailcow-dockerized) [![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/) +[![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/mailcow_email.svg?style=social&label=Follow%20%40mailcow_email)](https://twitter.com/mailcow_email) ## Want to support mailcow? diff --git a/create_cold_standby.sh b/create_cold_standby.sh old mode 100644 new mode 100755 diff --git a/data/Dockerfiles/clamd/Dockerfile b/data/Dockerfiles/clamd/Dockerfile index a4971133..cba56e03 100644 --- a/data/Dockerfiles/clamd/Dockerfile +++ b/data/Dockerfiles/clamd/Dockerfile @@ -8,8 +8,14 @@ RUN apk upgrade --no-cache \ bind-tools \ bash -COPY clamd.sh ./ +# init +COPY clamd.sh /clamd.sh RUN chmod +x /sbin/tini +# healthcheck +COPY healthcheck.sh /healthcheck.sh +RUN chmod +x /healthcheck.sh +HEALTHCHECK --start-period=6m CMD "/healthcheck.sh" + ENTRYPOINT [] CMD ["/sbin/tini", "-g", "--", "/clamd.sh"] \ No newline at end of file diff --git a/data/Dockerfiles/clamd/healthcheck.sh b/data/Dockerfiles/clamd/healthcheck.sh new file mode 100755 index 00000000..6c18ac06 --- /dev/null +++ b/data/Dockerfiles/clamd/healthcheck.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "SKIP_CLAMD=y, skipping ClamAV..." + exit 0 +fi + +# run clamd healthcheck +/usr/local/bin/clamdcheck.sh diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 4a914904..4e90052b 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -2,7 +2,7 @@ FROM debian:bullseye-slim LABEL maintainer "Andre Peters " ARG DEBIAN_FRONTEND=noninteractive -ARG DOVECOT=2.3.18 +ARG DOVECOT=2.3.19.1 ENV LC_ALL C ENV GOSU_VERSION 1.14 diff --git a/data/conf/rspamd/local.d/groups.conf b/data/conf/rspamd/local.d/groups.conf index 9ca3409d..f77d8a46 100644 --- a/data/conf/rspamd/local.d/groups.conf +++ b/data/conf/rspamd/local.d/groups.conf @@ -18,6 +18,9 @@ symbols { "ENCRYPTED_CHAT" { score = -20.0; } + "SOGO_CONTACT" { + score = -99.0; + } } group "MX" { diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index e9655751..3803e5bd 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -3953,6 +3953,8 @@ paths: in: query name: tags required: false + schema: + type: string - description: e.g. api-key-string example: api-key-string in: header @@ -4512,6 +4514,8 @@ paths: in: query name: tags required: false + schema: + type: string - description: e.g. api-key-string example: api-key-string in: header diff --git a/data/web/css/build/008-mailcow.css b/data/web/css/build/008-mailcow.css index c8c21b18..f557084f 100644 --- a/data/web/css/build/008-mailcow.css +++ b/data/web/css/build/008-mailcow.css @@ -260,6 +260,17 @@ code { margin-right: 5px; } +.list-group-item.webauthn-authenticator-selection, +.list-group-item.totp-authenticator-selection, +.list-group-item.yubi_otp-authenticator-selection { + border-radius: 0px !important; +} +.pending-tfa-collapse { + padding: 10px; + background: #fbfbfb; + border: 1px solid #ededed; +} + .tag-box { display: flex; flex-wrap: wrap; @@ -296,4 +307,3 @@ code { align-items: center; display: inline-flex; } - diff --git a/data/web/inc/ajax/destroy_tfa_auth.php b/data/web/inc/ajax/destroy_tfa_auth.php index 72c7f1e3..07873b55 100644 --- a/data/web/inc/ajax/destroy_tfa_auth.php +++ b/data/web/inc/ajax/destroy_tfa_auth.php @@ -2,5 +2,5 @@ session_start(); unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_role']); -unset($_SESSION['pending_tfa_method']); +unset($_SESSION['pending_tfa_methods']); ?> diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 72482707..b2f1d4d5 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -23,6 +23,27 @@ if (is_array($alertbox_log_parser)) { unset($_SESSION['return']); } +// map tfa details for twig +$pending_tfa_authmechs = []; +foreach($_SESSION['pending_tfa_methods'] as $authdata){ + $pending_tfa_authmechs[$authdata['authmech']] = false; +} +if (isset($pending_tfa_authmechs['webauthn'])) { + $pending_tfa_authmechs['webauthn'] = true; +} +if (!isset($pending_tfa_authmechs['webauthn']) + && isset($pending_tfa_authmechs['yubi_otp'])) { + $pending_tfa_authmechs['yubi_otp'] = true; +} +if (!isset($pending_tfa_authmechs['webauthn']) + && !isset($pending_tfa_authmechs['yubi_otp']) + && isset($pending_tfa_authmechs['totp'])) { + $pending_tfa_authmechs['totp'] = true; +} +if (isset($pending_tfa_authmechs['u2f'])) { + $pending_tfa_authmechs['u2f'] = true; +} + // globals $globalVariables = [ 'mailcow_info' => array( @@ -30,7 +51,8 @@ $globalVariables = [ 'git_project_url' => $GLOBALS['MAILCOW_GIT_URL'] ), 'js_path' => '/cache/'.basename($JSPath), - 'pending_tfa_method' => @$_SESSION['pending_tfa_method'], + 'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'], + 'pending_tfa_authmechs' => $pending_tfa_authmechs, 'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'], 'lang_footer' => json_encode($lang['footer']), 'lang_acl' => json_encode($lang['acl']), diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 21a0d8ce..08963888 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -830,11 +830,15 @@ function check_login($user, $pass, $app_passwd_data = false) { $stmt->execute(array(':user' => $user)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { + // verify password if (verify_hash($row['password'], $pass)) { - if (get_tfa($user)['name'] != "none") { + // check for tfa authenticators + $authenticators = get_tfa($user); + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { + // active tfa authenticators found, set pending user login $_SESSION['pending_mailcow_cc_username'] = $user; $_SESSION['pending_mailcow_cc_role'] = "admin"; - $_SESSION['pending_tfa_method'] = get_tfa($user)['name']; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; unset($_SESSION['ldelay']); $_SESSION['return'][] = array( 'type' => 'info', @@ -842,8 +846,7 @@ function check_login($user, $pass, $app_passwd_data = false) { 'msg' => 'awaiting_tfa_confirmation' ); return "pending"; - } - else { + } else { unset($_SESSION['ldelay']); // Reactivate TFA if it was set to "deactivate TFA for next login" $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); @@ -866,11 +869,14 @@ function check_login($user, $pass, $app_passwd_data = false) { $stmt->execute(array(':user' => $user)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { + // verify password if (verify_hash($row['password'], $pass) !== false) { - if (get_tfa($user)['name'] != "none") { + // check for tfa authenticators + $authenticators = get_tfa($user); + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { $_SESSION['pending_mailcow_cc_username'] = $user; $_SESSION['pending_mailcow_cc_role'] = "domainadmin"; - $_SESSION['pending_tfa_method'] = get_tfa($user)['name']; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; unset($_SESSION['ldelay']); $_SESSION['return'][] = array( 'type' => 'info', @@ -930,24 +936,39 @@ function check_login($user, $pass, $app_passwd_data = false) { $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); } foreach ($rows as $row) { + // verify password if (verify_hash($row['password'], $pass) !== false) { - unset($_SESSION['ldelay']); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => array('logged_in_as', $user) - ); - if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { - $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; - $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)"); - $stmt->execute(array( - ':service' => $service, - ':app_id' => $row['app_passwd_id'], - ':username' => $user, - ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) - )); + // check for tfa authenticators + $authenticators = get_tfa($user); + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { + $_SESSION['pending_mailcow_cc_username'] = $user; + $_SESSION['pending_mailcow_cc_role'] = "user"; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; + unset($_SESSION['ldelay']); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => array('logged_in_as', $user) + ); + return "pending"; + } else { + if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { + $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; + $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)"); + $stmt->execute(array( + ':service' => $service, + ':app_id' => $row['app_passwd_id'], + ':username' => $user, + ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) + )); + } + + unset($_SESSION['ldelay']); + // Reactivate TFA if it was set to "deactivate TFA for next login" + $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); + $stmt->execute(array(':user' => $user)); + return "user"; } - return "user"; } } @@ -1142,47 +1163,46 @@ function set_tfa($_data) { global $yubi; global $tfa; $_data_log = $_data; + $access_denied = null; !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*'; $username = $_SESSION['mailcow_cc_username']; - if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - $stmt = $pdo->prepare("SELECT `password` FROM `admin` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if (!empty($num_results)) { - if (!verify_hash($row['password'], $_data["confirm_password"])) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - } - $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if (!empty($num_results)) { - if (!verify_hash($row['password'], $_data["confirm_password"])) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; + + // check for empty user and role + if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true; + + // check admin confirm password + if ($access_denied === null) { + $stmt = $pdo->prepare("SELECT `password` FROM `admin` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($row) { + if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true; + else $access_denied = false; } } + // check mailbox confirm password + if ($access_denied === null) { + $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($row) { + if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true; + else $access_denied = false; + } + } + + // set access_denied error + if ($access_denied){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } switch ($_data["tfa_method"]) { case "yubi_otp": @@ -1220,8 +1240,7 @@ function set_tfa($_data) { $yubico_modhex_id = substr($_data["otp_token"], 0, 12); $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username - AND (`authmech` != 'yubi_otp') - OR (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)"); + AND (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)"); $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id)); $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES (:key_id, :username, 'yubi_otp', '1', :secret)"); @@ -1265,9 +1284,6 @@ function set_tfa($_data) { case "webauthn": $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"]; - $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'webauthn'"); - $stmt->execute(array(':username' => $username)); - $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')"); $stmt->execute(array( @@ -1439,25 +1455,27 @@ function unset_tfa_key($_data) { global $pdo; global $lang; $_data_log = $_data; + $access_denied = null; $id = intval($_data['unset_tfa_key']); $username = $_SESSION['mailcow_cc_username']; - if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } + + // check for empty user and role + if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true; + try { - if (!is_numeric($id)) { - $_SESSION['return'][] = array( + if (!is_numeric($id)) $access_denied = true; + + // set access_denied error + if ($access_denied){ + $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied' ); return false; - } + } + + // check if it's last key $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa` WHERE `username` = :username AND `active` = '1'"); $stmt->execute(array(':username' => $username)); @@ -1470,6 +1488,8 @@ function unset_tfa_key($_data) { ); return false; } + + // delete key $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id"); $stmt->execute(array(':username' => $username, ':id' => $id)); $_SESSION['return'][] = array( @@ -1487,7 +1507,7 @@ function unset_tfa_key($_data) { return false; } } -function get_tfa($username = null) { +function get_tfa($username = null, $id = null) { global $pdo; if (isset($_SESSION['mailcow_cc_username'])) { $username = $_SESSION['mailcow_cc_username']; @@ -1495,92 +1515,120 @@ function get_tfa($username = null) { elseif (empty($username)) { return false; } - $stmt = $pdo->prepare("SELECT * FROM `tfa` - WHERE `username` = :username AND `active` = '1'"); - $stmt->execute(array(':username' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (isset($row["authmech"])) { - switch ($row["authmech"]) { - case "yubi_otp": - $data['name'] = "yubi_otp"; - $data['pretty'] = "Yubico OTP"; - $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $data['additional'][] = $row; + if (!isset($id)){ + // fetch all tfa methods - just get information about possible authenticators + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `authmech` FROM `tfa` + WHERE `username` = :username AND `active` = '1'"); + $stmt->execute(array(':username' => $username)); + $results = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // no tfa methods found + if (count($results) == 0) { + $data['name'] = 'none'; + $data['pretty'] = "-"; + $data['additional'] = array(); + return $data; + } + + $data['additional'] = $results; + return $data; + } else { + // fetch specific authenticator details by id + $stmt = $pdo->prepare("SELECT * FROM `tfa` + WHERE `username` = :username AND `id` = :id AND `active` = '1'"); + $stmt->execute(array(':username' => $username, ':id' => $id)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if (isset($row["authmech"])) { + switch ($row["authmech"]) { + case "yubi_otp": + $data['name'] = "yubi_otp"; + $data['pretty'] = "Yubico OTP"; + $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username AND `id` = :id"); + $stmt->execute(array( + ':username' => $username, + ':id' => $id + )); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $data['additional'][] = $row; + } + return $data; + break; + // u2f - deprecated, should be removed + case "u2f": + $data['name'] = "u2f"; + $data['pretty'] = "Fido U2F"; + $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username AND `id` = :id"); + $stmt->execute(array( + ':username' => $username, + ':id' => $id + )); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $data['additional'][] = $row; + } + return $data; + break; + case "hotp": + $data['name'] = "hotp"; + $data['pretty'] = "HMAC-based OTP"; + return $data; + break; + case "totp": + $data['name'] = "totp"; + $data['pretty'] = "Time-based OTP"; + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username AND `id` = :id"); + $stmt->execute(array( + ':username' => $username, + ':id' => $id + )); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $data['additional'][] = $row; + } + return $data; + break; + case "webauthn": + $data['name'] = "webauthn"; + $data['pretty'] = "WebAuthn"; + $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username AND `id` = :id"); + $stmt->execute(array( + ':username' => $username, + ':id' => $id + )); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $data['additional'][] = $row; + } + return $data; + break; + default: + $data['name'] = 'none'; + $data['pretty'] = "-"; + return $data; + break; } - return $data; - break; - // u2f - deprecated, should be removed - case "u2f": - $data['name'] = "u2f"; - $data['pretty'] = "Fido U2F"; - $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $data['additional'][] = $row; - } - return $data; - break; - case "hotp": - $data['name'] = "hotp"; - $data['pretty'] = "HMAC-based OTP"; - return $data; - break; - case "totp": - $data['name'] = "totp"; - $data['pretty'] = "Time-based OTP"; - $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $data['additional'][] = $row; - } - return $data; - break; - case "webauthn": - $data['name'] = "webauthn"; - $data['pretty'] = "WebAuthn"; - $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $data['additional'][] = $row; - } - return $data; - break; - default: + } + else { $data['name'] = 'none'; $data['pretty'] = "-"; return $data; - break; + } } - } - else { - $data['name'] = 'none'; - $data['pretty'] = "-"; - return $data; - } } -function verify_tfa_login($username, $_data, $WebAuthn) { - global $pdo; - global $yubi; - global $u2f; - global $tfa; +function verify_tfa_login($username, $_data) { + global $pdo; + global $yubi; + global $u2f; + global $tfa; + global $WebAuthn; + + if ($_data['tfa_method'] != 'u2f'){ $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` - WHERE `username` = :username AND `active` = '1'"); - $stmt->execute(array(':username' => $username)); + WHERE `username` = :username AND `id` = :id AND `active` = '1'"); + $stmt->execute(array(':username' => $username, ':id' => $_data['id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); switch ($row["authmech"]) { @@ -1597,9 +1645,10 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'yubi_otp' - AND `active`='1' + AND `id` = :id + AND `active` = '1' AND `secret` LIKE :modhex"); - $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id)); + $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id, ':id' => $_data['id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); $yubico_auth = explode(':', $row['secret']); $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]); @@ -1632,15 +1681,16 @@ function verify_tfa_login($username, $_data, $WebAuthn) { return false; break; case "totp": - try { + try { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'totp' + AND `id` = :id AND `active`='1'"); - $stmt->execute(array(':username' => $username)); + $stmt->execute(array(':username' => $username, ':id' => $_data['id'])); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { - if ($tfa->verifyCode($row['secret'], $_data['token']) === true) { + if ($tfa->verifyCode($row['secret'], $_data['token']) === true) { $_SESSION['tfa_id'] = $row['id']; $_SESSION['return'][] = array( 'type' => 'success', @@ -1648,7 +1698,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) { 'msg' => 'verified_totp_login' ); return true; - } + } } $_SESSION['return'][] = array( 'type' => 'danger', @@ -1656,23 +1706,16 @@ function verify_tfa_login($username, $_data, $WebAuthn) { 'msg' => 'totp_verification_failed' ); return false; - } - catch (PDOException $e) { + } + catch (PDOException $e) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), 'msg' => array('mysql_error', $e) ); return false; - } + } break; - // u2f - deprecated, should be removed - case "u2f": - // delete old keys that used u2f - $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = :authmech AND `username` = :username"); - $stmt->execute(array(':authmech' => 'u2f', ':username' => $username)); - - return true; case "webauthn": $tokenData = json_decode($_data['token']); $clientDataJSON = base64_decode($tokenData->clientDataJSON); @@ -1681,13 +1724,20 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $id = base64_decode($tokenData->id); $challenge = $_SESSION['challenge']; - $stmt = $pdo->prepare("SELECT `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId"); - $stmt->execute(array(':tokenId' => $tokenData->id)); + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id AND `active`='1'"); + $stmt->execute(array(':id' => $_data['id'])); $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($process_webauthn) || empty($process_webauthn['publicKey']) || empty($process_webauthn['username'])) return false; + if (empty($process_webauthn)){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $username, '*'), + 'msg' => array('webauthn_verification_failed', 'authenticator not found') + ); + return false; + } - if ($process_webauthn['publicKey'] === false) { + if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), @@ -1695,6 +1745,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) { ); return false; } + try { $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']); } @@ -1707,26 +1758,31 @@ function verify_tfa_login($username, $_data, $WebAuthn) { return false; } - $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username"); $stmt->execute(array(':username' => $process_webauthn['username'])); $obj_props = $stmt->fetch(PDO::FETCH_ASSOC); if ($obj_props['superadmin'] === 1) { - $_SESSION["mailcow_cc_role"] = "admin"; + $_SESSION["mailcow_cc_role"] = "admin"; } elseif ($obj_props['superadmin'] === 0) { - $_SESSION["mailcow_cc_role"] = "domainadmin"; + $_SESSION["mailcow_cc_role"] = "domainadmin"; } else { - $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username"); - $stmt->execute(array(':username' => $process_webauthn['username'])); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if ($row['username'] == $process_webauthn['username']) { + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username"); + $stmt->execute(array(':username' => $process_webauthn['username'])); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (!empty($row['username'])) { $_SESSION["mailcow_cc_role"] = "user"; - } + } else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $username, '*'), + 'msg' => array('webauthn_verification_failed', 'could not determine user role') + ); + return false; + } } - if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){ $_SESSION['return'][] = array( 'type' => 'danger', @@ -1736,9 +1792,8 @@ function verify_tfa_login($username, $_data, $WebAuthn) { return false; } - $_SESSION["mailcow_cc_username"] = $process_webauthn['username']; - $_SESSION['tfa_id'] = $process_webauthn['key_id']; + $_SESSION['tfa_id'] = $process_webauthn['id']; $_SESSION['authReq'] = null; unset($_SESSION["challenge"]); $_SESSION['return'][] = array( @@ -1759,6 +1814,17 @@ function verify_tfa_login($username, $_data, $WebAuthn) { } return false; + } else { + // delete old keys that used u2f + $stmt = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username"); + $stmt->execute(array(':username' => $username)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + if (count($rows) == 0) return false; + + $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username"); + $stmt->execute(array(':username' => $username)); + return true; + } } function admin_api($access, $action, $data = null) { global $pdo; diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 788b207f..2cf9f6c6 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -338,9 +338,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $custom_params = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']); // validate custom params - foreach (explode(' -', $custom_params) as $param){ + foreach (explode('-', $custom_params) as $param){ if(empty($param)) continue; + // extract option + if (str_contains($param, '=')) $param = explode('=', $param)[0]; + else $param = rtrim($param, ' '); + // remove first char if first char is - + if ($param[0] == '-') $param = ltrim($param, $param[0]); + if (str_contains($param, ' ')) { // bad char $_SESSION['return'][] = array( @@ -351,11 +357,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { return false; } - // extract option - if (str_contains($param, '=')) $param = explode('=', $param)[0]; - // remove first char if first char is - - if ($param[0] == '-') $param = ltrim($param, $param[0]); - // check if param is whitelisted if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){ // bad option @@ -1793,9 +1794,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } // validate custom params - foreach (explode(' -', $custom_params) as $param){ + foreach (explode('-', $custom_params) as $param){ if(empty($param)) continue; + // extract option + if (str_contains($param, '=')) $param = explode('=', $param)[0]; + else $param = rtrim($param, ' '); + // remove first char if first char is - + if ($param[0] == '-') $param = ltrim($param, $param[0]); + if (str_contains($param, ' ')) { // bad char $_SESSION['return'][] = array( @@ -1806,11 +1813,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { return false; } - // extract option - if (str_contains($param, '=')) $param = explode('=', $param)[0]; - // remove first char if first char is - - if ($param[0] == '-') $param = ltrim($param, $param[0]); - // check if param is whitelisted if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){ // bad option diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 1e53d4b8..c80c398b 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "18062022_1153"; + $db_version = "13072022_1700"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -739,7 +739,7 @@ function init_db_schema() { "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')", "secret" => "VARCHAR(255) DEFAULT NULL", "keyHandle" => "VARCHAR(255) DEFAULT NULL", - "publicKey" => "VARCHAR(255) DEFAULT NULL", + "publicKey" => "VARCHAR(4096) DEFAULT NULL", "counter" => "INT NOT NULL DEFAULT '0'", "certificate" => "TEXT", "active" => "TINYINT(1) NOT NULL DEFAULT '0'" @@ -1227,7 +1227,7 @@ function init_db_schema() { $pdo->query($create); } - // Mitigate imapsync pipemess issue + // Mitigate imapsync argument injection issue $pdo->query("UPDATE `imapsync` SET `custom_params` = '' WHERE `custom_params` LIKE '%pipemess%' OR custom_params LIKE '%skipmess%' @@ -1237,8 +1237,7 @@ function init_db_schema() { OR custom_params LIKE '%pipemess%' OR custom_params LIKE '%regextrans2%' OR custom_params LIKE '%maxlinelengthcmd%';"); - - + // Migrate webauthn tfa $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')"); diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 6cfdd1ca..2897444b 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -66,8 +66,9 @@ $qrprovider = new RobThree\Auth\Providers\Qr\QRServerProvider(); $tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider); // FIDO2 +$server_name = parse_url('https://' . $_SERVER['HTTP_HOST'], PHP_URL_HOST); $formats = $GLOBALS['FIDO2_FORMATS']; -$WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $_SERVER['HTTP_HOST'], $formats); +$WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $server_name, $formats); // only include root ca's when needed if (getenv('WEBAUTHN_ONLY_TRUSTED_VENDORS') == 'y') $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates'); diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index cb3a3771..aec043e9 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -1,24 +1,24 @@ array( - 'log', - 'showpasswords', - 'nossl1', - 'nossl2', - 'ssl2', - 'notls1', - 'notls2', - 'tls2', - 'debugssl', - 'sslargs1', - 'sslargs2', - 'authmech1', - 'authmech2', - 'authuser1', - 'authuser2', - 'proxyauth1', - 'proxyauth2', - 'authmd51', - 'authmd52', - 'domain1', - 'domain2', - 'oauthaccesstoken1', - 'oauthaccesstoken2', - 'oauthdirect1', - 'oauthdirect2', - 'folder', - 'folder', - 'folderrec', - 'folderrec', - 'folderfirst', - 'folderfirst', - 'folderlast', - 'folderlast', - 'nomixfolders', - 'skipemptyfolders', - 'include', - 'include', - 'subfolder1', - 'subscribed', - 'subscribe', - 'prefix1', - 'prefix2', - 'sep1', - 'sep2', - 'nofoldersizesatend', - 'justfoldersizes', - 'pidfile', - 'pidfilelocking', - 'nolog', - 'logfile', - 'logdir', - 'debugcrossduplicates', - 'disarmreadreceipts', - 'truncmess', - 'synclabels', - 'resynclabels', - 'resyncflags', - 'noresyncflags', - 'filterbuggyflags', - 'expunge1', - 'noexpunge1', - 'delete1emptyfolders', - 'delete2folders', - 'noexpunge2', - 'nouidexpunge2', - 'syncinternaldates', - 'idatefromheader', - 'maxsize', - 'minsize', - 'minage', - 'search', - 'search1', - 'search2', - 'noabletosearch', - 'noabletosearch1', - 'noabletosearch2', - 'maxlinelength', - 'useheader', - 'useheader', - 'syncduplicates', - 'usecache', - 'nousecache', - 'useuid', - 'syncacls', - 'nosyncacls', - 'debug', - 'debugfolders', - 'debugcontent', - 'debugflags', - 'debugimap1', - 'debugimap2', - 'debugimap', - 'debugmemory', - 'errorsmax', - 'tests', - 'testslive', - 'testslive6', - 'gmail1', - 'gmail2', - 'office1', - 'office2', - 'exchange1', - 'exchange2', - 'domino1', - 'domino2', - 'keepalive1', - 'keepalive2', - 'maxmessagespersecond', - 'maxbytesafter', - 'maxsleep', - 'abort', - 'exitwhenover', - 'noid', - 'justconnect', - 'justlogin', - 'justfolders' + 'authmech1', + 'authmech2', + 'authuser1', + 'authuser2', + 'debugcontent', + 'disarmreadreceipts', + 'logdir', + 'debugcrossduplicates', + 'maxsize', + 'minsize', + 'minage', + 'search', + 'noabletosearch', + 'pidfile', + 'pidfilelocking', + 'search1', + 'search2', + 'sslargs1', + 'sslargs2', + 'syncduplicates', + 'usecache', + 'synclabels', + 'truncmess', + 'domino2', + 'expunge1', + 'filterbuggyflags', + 'justconnect', + 'justfolders', + 'maxlinelength', + 'useheader', + 'noabletosearch1', + 'nolog', + 'prefix1', + 'prefix2', + 'sep1', + 'sep2', + 'nofoldersizesatend', + 'justfoldersizes', + 'proxyauth1', + 'skipemptyfolders', + 'include', + 'subfolder1', + 'subscribed', + 'subscribe', + 'debug', + 'debugimap2', + 'domino1', + 'exchange1', + 'exchange2', + 'justlogin', + 'keepalive1', + 'keepalive2', + 'noabletosearch2', + 'noexpunge2', + 'noresyncflags', + 'nossl1', + 'nouidexpunge2', + 'syncinternaldates', + 'idatefromheader', + 'useuid', + 'debugflags', + 'debugimap', + 'delete1emptyfolders', + 'delete2folders', + 'gmail2', + 'office1', + 'testslive6', + 'debugimap1', + 'errorsmax', + 'tests', + 'gmail1', + 'maxmessagespersecond', + 'maxbytesafter', + 'maxsleep', + 'abort', + 'resyncflags', + 'resynclabels', + 'syncacls', + 'nosyncacls', + 'nousecache', + 'office2', + 'testslive', + 'debugmemory', + 'exitwhenover', + 'noid', + 'noexpunge1', + 'authmd51', + 'logfile', + 'proxyauth2', + 'domain1', + 'domain2', + 'oauthaccesstoken1', + 'oauthaccesstoken2', + 'oauthdirect1', + 'oauthdirect2', + 'folder', + 'folderrec', + 'folderfirst', + 'folderlast', + 'nomixfolders', + 'authmd52', + 'debugfolders', + 'nossl2', + 'ssl2', + 'tls2', + 'notls2', + 'debugssl', + 'notls1', + 'inet4', + 'inet6', + 'log', + 'showpasswords' ), 'blacklist' => array( - 'skipmess', - 'delete2foldersonly', - 'delete2foldersbutnot', - 'regexflag', - 'regexmess', - 'pipemess', - 'regextrans2', - 'maxlinelengthcmd' + 'skipmess', + 'delete2foldersonly', + 'delete2foldersbutnot', + 'regexflag', + 'regexmess', + 'pipemess', + 'regextrans2', + 'maxlinelengthcmd' ) ); diff --git a/data/web/json_api.php b/data/web/json_api.php index 8db4ef89..22b747bc 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -178,15 +178,22 @@ if (isset($_GET['query'])) { // parse post data $post = trim(file_get_contents('php://input')); if ($post) $post = json_decode($post); - - // decode base64 strings - $clientDataJSON = base64_decode($post->clientDataJSON); - $attestationObject = base64_decode($post->attestationObject); // process registration data from authenticator try { + // decode base64 strings + $clientDataJSON = base64_decode($post->clientDataJSON); + $attestationObject = base64_decode($post->attestationObject); + // processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true) $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true); + + // safe authenticator in mysql `tfa` table + $_data['tfa_method'] = $post->tfa_method; + $_data['key_id'] = $post->key_id; + $_data['confirm_password'] = $post->confirm_password; + $_data['registration'] = $data; + set_tfa($_data); } catch (Throwable $ex) { // err @@ -197,11 +204,6 @@ if (isset($_GET['query'])) { exit; } - // safe authenticator in mysql `tfa` table - $_data['tfa_method'] = $post->tfa_method; - $_data['key_id'] = $post->key_id; - $_data['registration'] = $data; - set_tfa($_data); // send response $return = new stdClass(); @@ -419,7 +421,7 @@ if (isset($_GET['query'])) { // } $ids = NULL; - $getArgs = $WebAuthn->getGetArgs($ids, 30, true, true, true, true, $GLOBALS['FIDO2_UV_FLAG_LOGIN']); + $getArgs = $WebAuthn->getGetArgs($ids, 30, false, false, false, false, $GLOBALS['FIDO2_UV_FLAG_LOGIN']); print(json_encode($getArgs)); $_SESSION['challenge'] = $WebAuthn->getChallenge(); return; @@ -428,8 +430,11 @@ if (isset($_GET['query'])) { case "webauthn-tfa-registration": if (isset($_SESSION["mailcow_cc_role"])) { // Exclude existing CredentialIds, if any - $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username"); - $stmt->execute(array(':username' => $_SESSION['mailcow_cc_username'])); + $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech"); + $stmt->execute(array( + ':username' => $_SESSION['mailcow_cc_username'], + ':authmech' => 'webauthn' + )); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { $excludeCredentialIds[] = base64_decode($row['keyHandle']); @@ -450,20 +455,24 @@ if (isset($_GET['query'])) { } break; case "webauthn-tfa-get-args": - $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username"); - $stmt->execute(array(':username' => $_SESSION['pending_mailcow_cc_username'])); + $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech"); + $stmt->execute(array( + ':username' => $_SESSION['pending_mailcow_cc_username'], + ':authmech' => 'webauthn' + )); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $cids[] = base64_decode($row['keyHandle']); - } - if (count($cids) == 0) { + if (count($rows) == 0) { print(json_encode(array( 'type' => 'error', 'msg' => 'Cannot find matching credentialIds' ))); + exit; + } + while($row = array_shift($rows)) { + $cids[] = base64_decode($row['keyHandle']); } - $getArgs = $WebAuthn->getGetArgs($cids, 30, true, true, true, true, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']); + $getArgs = $WebAuthn->getGetArgs($cids, 30, false, false, false, false, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']); $getArgs->publicKey->extensions = array('appid' => "https://".$getArgs->publicKey->rpId); print(json_encode($getArgs)); $_SESSION['challenge'] = $WebAuthn->getChallenge(); diff --git a/data/web/lang/lang.ru.json b/data/web/lang/lang.ru.json index dd1418e4..c9ffad6d 100644 --- a/data/web/lang/lang.ru.json +++ b/data/web/lang/lang.ru.json @@ -988,7 +988,7 @@ "enter_qr_code": "Ваш код TOTP, если устройство не может отсканировать QR-код", "error_code": "Код ошибки", "init_webauthn": "Инициализация, пожалуйста, подождите...", - "key_id": "Идентификатор YubiKey ключа", + "key_id": "Идентификатор вашего устройства", "key_id_totp": "Идентификатор TOTP ключа", "none": "Отключить", "reload_retry": "- (перезагрузить страницу браузера или почистите кеш/cookies, если ошибка повторяется)", @@ -1002,7 +1002,8 @@ "webauthn": "WebAuthn аутентификация", "waiting_usb_auth": "Ожидание устройства USB...

Пожалуйста, нажмите кнопку на USB устройстве сейчас.", "waiting_usb_register": "Ожидание устройства USB...

Пожалуйста, введите пароль выше и подтвердите регистрацию, нажав кнопку на USB устройстве.", - "yubi_otp": "Yubico OTP аутентификация" + "yubi_otp": "Yubico OTP аутентификация", + "u2f_deprecated": "Похоже, что ваш ключ был зарегистрирован с использованием устаревшего метода U2F. Мы деактивируем для вас двухфакторную аутентификацию и удалим ваш ключ." }, "user": { "action": "Действия", diff --git a/data/web/lang/lang.uk.json b/data/web/lang/lang.uk.json index 0b8df84e..48aba504 100644 --- a/data/web/lang/lang.uk.json +++ b/data/web/lang/lang.uk.json @@ -980,7 +980,8 @@ "resource_modified": "Зміни поштового акаунту %s збережено", "settings_map_added": "Правило додано", "tls_policy_map_entry_deleted": "Політику TLS ID %s видалено", - "verified_totp_login": "Авторизацію TOTP пройдено" + "verified_totp_login": "Авторизацію TOTP пройдено", + "domain_add_dkim_available": "Ключ DKIM вже існує" }, "tfa": { "confirm": "Підтвердьте", diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 08376e71..37c27e21 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -176,15 +176,62 @@ function recursiveBase64StrToArrayBuffer(obj) { {% endfor %} // Confirm TFA modal - {% if pending_tfa_method %} + {% if pending_tfa_methods %} $('#ConfirmTFAModal').modal({ backdrop: 'static', keyboard: false }); + // validate Yubi OTP tfa + $("#pending_tfa_tab_yubi_otp").click(function(){ + $(".totp-authenticator-selection").removeClass("active"); + $(".webauthn-authenticator-selection").removeClass("active"); + + $("#collapseTotpTFA").collapse('hide'); + $("#collapseWebAuthnTFA").collapse('hide'); + }); + $(".yubi-authenticator-selection").click(function(){ + $(".yubi-authenticator-selection").removeClass("active"); + $(this).addClass("active"); + + var id = $(this).children('input').first().val(); + $("#yubi_selected_id").val(id); + + $("#collapseYubiTFA").collapse('show'); + }); + // validate Time based OTP tfa + $("#pending_tfa_tab_totp").click(function(){ + $(".yubi-authenticator-selection").removeClass("active"); + $(".webauthn-authenticator-selection").removeClass("active"); + + $("#collapseYubiTFA").collapse('hide'); + $("#collapseWebAuthnTFA").collapse('hide'); + }); + $(".totp-authenticator-selection").click(function(){ + $(".totp-authenticator-selection").removeClass("active"); + $(this).addClass("active"); + + var id = $(this).children('input').first().val(); + $("#totp_selected_id").val(id); + + $("#collapseTotpTFA").collapse('show'); + }); // validate WebAuthn tfa - $('#start_webauthn_confirmation').click(function(){ - $('#webauthn_status_auth').html('

' + lang_tfa.init_webauthn + '

'); + $("#pending_tfa_tab_webauthn").click(function(){ + $(".totp-authenticator-selection").removeClass("active"); + $(".yubi-authenticator-selection").removeClass("active"); + + $("#collapseTotpTFA").collapse('hide'); + $("#collapseYubiTFA").collapse('hide'); + }); + $(".webauthn-authenticator-selection").click(function(){ + $(".webauthn-authenticator-selection").removeClass("active"); + $(this).addClass("active"); + + var id = $(this).children('input').first().val(); + $("#webauthn_selected_id").val(id); + + $("#collapseWebAuthnTFA").collapse('show'); $(this).find('input[name=token]').focus(); if(document.getElementById("webauthn_auth_data") !== null) { @@ -198,30 +245,32 @@ function recursiveBase64StrToArrayBuffer(obj) { window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => { return response.json(); }).then(json => { - if (json.success === false) throw new Error(); + console.log(json); + if (json.success === false) throw new Error(); + if (json.type === "error") throw new Error(json.msg); - recursiveBase64StrToArrayBuffer(json); - return json; + recursiveBase64StrToArrayBuffer(json); + return json; }).then(getCredentialArgs => { - // get credentials - return navigator.credentials.get(getCredentialArgs); + // get credentials + return navigator.credentials.get(getCredentialArgs); }).then(cred => { - return { - id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, - clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, - authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, - signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null - }; + return { + id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, + clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, + authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, + signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null + }; }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) { - // send request by submit - var form = document.getElementById('webauthn_auth_form'); - var auth = document.getElementById('webauthn_auth_data'); - auth.value = AuthenticatorAttestationResponse; - form.submit(); + // send request by submit + var form = document.getElementById('webauthn_auth_form'); + var auth = document.getElementById('webauthn_auth_data'); + auth.value = AuthenticatorAttestationResponse; + form.submit(); }).catch(function(err) { - var webauthn_return_code = document.getElementById('webauthn_return_code'); - webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null; - webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; + var webauthn_return_code = document.getElementById('webauthn_return_code'); + webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null; + webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; }); } }); @@ -237,7 +286,9 @@ function recursiveBase64StrToArrayBuffer(obj) { } }); }); - {% endif %} + {% endif %} + + // Validate FIDO2 $("#fido2-login").click(function(){ $('#fido2-alerts').html(); @@ -358,11 +409,13 @@ function recursiveBase64StrToArrayBuffer(obj) { $("#start_webauthn_register").click(() => { var key_id = document.getElementsByName('key_id')[1].value; + var confirm_password = document.getElementsByName('confirm_password')[1].value; // fetch WebAuthn create args window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => { return response.json(); }).then(json => { + console.log(json); if (json.success === false) throw new Error(json.msg); recursiveBase64StrToArrayBuffer(json); @@ -375,7 +428,8 @@ function recursiveBase64StrToArrayBuffer(obj) { clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null, key_id: key_id, - tfa_method: "webauthn" + tfa_method: "webauthn", + confirm_password: confirm_password }; }).then(JSON.stringify).then(AuthenticatorAttestationResponse => { // send request diff --git a/data/web/templates/domainadmin.twig b/data/web/templates/domainadmin.twig index 6aae54ba..3bc7ab40 100644 --- a/data/web/templates/domainadmin.twig +++ b/data/web/templates/domainadmin.twig @@ -28,7 +28,7 @@
diff --git a/data/web/templates/modals/footer.twig b/data/web/templates/modals/footer.twig index 690b9de0..67cc3482 100644 --- a/data/web/templates/modals/footer.twig +++ b/data/web/templates/modals/footer.twig @@ -133,73 +133,174 @@
{% endif %} -{% if pending_tfa_method %} +{% if pending_tfa_methods %} diff --git a/data/web/templates/modals/mailbox.twig b/data/web/templates/modals/mailbox.twig index 4097145f..17e06ed2 100644 --- a/data/web/templates/modals/mailbox.twig +++ b/data/web/templates/modals/mailbox.twig @@ -435,11 +435,11 @@

{{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}∞{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}
{{ mailboxdata.messages }} {{ lang.user.messages }}

-
-

{{ lang.user.change_password }}

+ + +
+ {# TFA #} +
+
{{ lang.tfa.tfa }}:
+
+

{{ tfa_data.pretty }}

+ {% include 'tfa_keys.twig' %} +
+
+
+
+
{{ lang.tfa.set_tfa }}:
+
+

diff --git a/data/web/user.php b/data/web/user.php index 5bf60917..d8fc80c1 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -76,6 +76,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' 'acl_json' => json_encode($_SESSION['acl']), 'user_spam_score' => mailbox('get', 'spam_score', $username), 'tfa_data' => $tfa_data, + 'tfa_id' => @$_SESSION['tfa_id'], 'fido2_data' => $fido2_data, 'mailboxdata' => $mailboxdata, 'clientconfigstr' => $clientconfigstr, @@ -90,8 +91,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' 'number_of_app_passwords' => $number_of_app_passwords, ]; } - -if (!isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { +else { header('Location: /'); exit(); } diff --git a/docker-compose.yml b/docker-compose.yml index a563df91..65a9d3e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,7 +58,7 @@ services: - redis clamd-mailcow: - image: mailcow/clamd:1.52 + image: mailcow/clamd:1.53 restart: always depends_on: - unbound-mailcow @@ -168,7 +168,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.108 + image: mailcow/sogo:1.109 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} @@ -215,7 +215,7 @@ services: - sogo dovecot-mailcow: - image: mailcow/dovecot:1.162 + image: mailcow/dovecot:1.17 depends_on: - mysql-mailcow dns: diff --git a/helper-scripts/_cold-standby.sh b/helper-scripts/_cold-standby.sh index 7fc5a495..fadee6f6 100755 --- a/helper-scripts/_cold-standby.sh +++ b/helper-scripts/_cold-standby.sh @@ -271,4 +271,13 @@ if ! ssh -o StrictHostKeyChecking=no \ >&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote" fi +echo -e "\033[1mExecuting update script and checking for new docker-compose Version on remote...\033[0m" +if ! ssh -o StrictHostKeyChecking=no \ + -i "${REMOTE_SSH_KEY}" \ + ${REMOTE_SSH_HOST} \ + -p ${REMOTE_SSH_PORT} \ + ${SCRIPT_DIR}/../update.sh -f --update-compose ; then + >&2 echo -e "\e[31m[ERR]\e[0m - Could not fetch docker-compose on remote" +fi + echo -e "\e[32mDone\e[0m" diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index d6f11ca8..506420b4 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -76,13 +76,6 @@ else CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]") fi -for bin in docker docker-compose; do - if [[ -z $(which ${bin}) ]]; then - >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" - exit 1 - fi -done - if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then >&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m" exit 1 @@ -94,6 +87,12 @@ function backup() { mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}" chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}" cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}" + for bin in docker; do + if [[ -z $(which ${bin}) ]]; then + >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" + exit 1 + fi + done while (( "$#" )); do case "$1" in vmail|all) @@ -161,6 +160,12 @@ function backup() { } function restore() { + for bin in docker docker-compose; do + if [[ -z $(which ${bin}) ]]; then + >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" + exit 1 + fi + done echo echo "Stopping watchdog-mailcow..." docker stop $(docker ps -qf name=watchdog-mailcow) @@ -354,4 +359,4 @@ elif [[ ${1} == "restore" ]]; then done echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..." restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]} -fi +fi \ No newline at end of file diff --git a/update.sh b/update.sh index ea283eb2..cd9d4fb6 100755 --- a/update.sh +++ b/update.sh @@ -1,64 +1,6 @@ #!/usr/bin/env bash -# Check permissions -if [ "$(id -u)" -ne "0" ]; then - echo "You need to be root" - exit 1 -fi - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -# Run pre-update-hook -if [ -f "${SCRIPT_DIR}/pre_update_hook.sh" ]; then - bash "${SCRIPT_DIR}/pre_update_hook.sh" -fi - -if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then - echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"; - echo "Please update to 5.x or use another distribution." - exit 1 -fi - -if [[ "$(uname -r)" =~ ^4\.4\. ]]; then - if grep -q Ubuntu <<< $(uname -a); then - echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!" - echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\"" - exit 1 - fi - echo "mailcow on a 4.4.x kernel is not supported. It may or may not work, please upgrade your kernel or continue at your own risk." - read -p "Press any key to continue..." < /dev/tty -fi - -# Exit on error and pipefail -set -o pipefail - -# Setting high dc timeout -export COMPOSE_HTTP_TIMEOUT=600 - -# Add /opt/bin to PATH -PATH=$PATH:/opt/bin - -umask 0022 - -for bin in curl docker git awk sha1sum; do - if [[ -z $(which ${bin}) ]]; then - echo "Cannot find ${bin}, exiting..." - exit 1; - elif [[ -z $(which docker-compose) ]]; then - echo "Cannot find docker-compose Standalone. Installing..." - sleep 3 - if [[ -e /etc/alpine-release ]]; then - echo -e "\e[33mNot installing latest docker-compose, because you are using Alpine Linux without glibc support. Install docker-compose via apk!\e[0m" - exit 1 - fi - curl -#L https://github.com/docker/compose/releases/download/v$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose - chmod +x /usr/local/bin/docker-compose - fi -done - -export LC_ALL=C -DATE=$(date +%Y-%m-%d_%H_%M_%S) -BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD) +############## Begin Function Section ############## check_online_status() { CHECK_ONLINE_IPS=(1.1.1.1 9.9.9.9 8.8.8.8) @@ -223,7 +165,7 @@ remove_obsolete_nginx_ports() { sed -i '/nginx-mailcow:$/,/^$/d' $override echo -e "\e[33mRemoved obsolete NGINX IPv6 Bind from original override File.\e[0m" if [[ "$(cat $override | sed '/^\s*$/d' | wc -l)" == "2" ]]; then - mv $override ${override}_backup + mv $override ${override}_empty echo -e "\e[31m${override} is empty. Renamed it to ensure mailcow is startable.\e[0m" fi fi @@ -241,6 +183,13 @@ elif [[ -e /etc/alpine-release ]]; then echo -e "\e[33mNot fetching latest docker-compose, because you are using Alpine Linux without glibc support. Please update docker-compose via apk!\e[0m" return 0 else + if [ ! $FORCE ]; then + read -r -p "Do you want to update your docker-compose Version? It will automatic upgrade your docker-compose installation (recommended)? [y/N] " updatecomposeresponse + if [[ ! "${updatecomposeresponse}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "OK, not updating docker-compose." + return 0 + fi + fi echo -e "\e[32mFetching new docker-compose version...\e[0m" echo -e "\e[32mTrying to determine GLIBC version...\e[0m" if ldd --version > /dev/null; then @@ -254,8 +203,12 @@ else DC_DL_SUFFIX=legacy fi sleep 1 - if [[ ! -z $(which pip) && $(pip list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 ]]; then - true + if [[ $(which pip 2>&1) && $(pip list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 || $(which pip3 2>&1) && $(pip3 list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 ]]; then + echo -e "\e[33mFound a docker-compose Version installed with pip!\e[0m" + echo -e "\e[31mPlease uninstall the pip Version of docker-compose since it doesn´t support Versions higher than 1.29.2.\e[0m" + sleep 2 + echo -e "\e[33mExiting...\e[0m" + exit 1 #prevent breaking a working docker-compose installed with pip elif [[ $(curl -sL -w "%{http_code}" https://www.servercow.de/docker-compose/latest.php?vers=${DC_DL_SUFFIX} -o /dev/null) == "200" ]]; then LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php) @@ -277,6 +230,79 @@ else fi } +############## End Function Section ############## + +# Check permissions +if [ "$(id -u)" -ne "0" ]; then + echo "You need to be root" + exit 1 +fi + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Run pre-update-hook +if [ -f "${SCRIPT_DIR}/pre_update_hook.sh" ]; then + bash "${SCRIPT_DIR}/pre_update_hook.sh" +fi + +if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then + echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"; + echo "Please update to 5.x or use another distribution." + exit 1 +fi + +if [[ "$(uname -r)" =~ ^4\.4\. ]]; then + if grep -q Ubuntu <<< $(uname -a); then + echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!" + echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\"" + exit 1 + fi + echo "mailcow on a 4.4.x kernel is not supported. It may or may not work, please upgrade your kernel or continue at your own risk." + read -p "Press any key to continue..." < /dev/tty +fi + +# Exit on error and pipefail +set -o pipefail + +# Setting high dc timeout +export COMPOSE_HTTP_TIMEOUT=600 + +# Add /opt/bin to PATH +PATH=$PATH:/opt/bin + +umask 0022 + +for bin in curl docker git awk sha1sum; do + if [[ -z $(which ${bin}) ]]; then + echo "Cannot find ${bin}, exiting..." + exit 1; + elif [[ -z $(which docker-compose) ]]; then + echo -e "\e[31mCannot find docker-compose Standalone.\e[0m" + echo -e "\e[31mPlease install it manually regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m" + sleep 3 + exit 1; + fi +done + +## Check if docker-compose >= v2 +if ! docker-compose version --short | grep "^2." > /dev/null 2>&1; then + echo -e "\e[33mYour docker-compose Version is not up to date!\e[0m" + echo -e "\e[33mmailcow needs docker-compose > 2.X.X!\e[0m" + echo -e "\e[33mYour current installed Version: $(docker-compose version --short)\e[0m" + sleep 3 + update_compose + if [[ ! "${updatecomposeresponse}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo -e "\e[31mmailcow does not work with docker-compose < 2.X.X anymore!\e[0m" + echo -e "\e[31mPlease update your docker-compose manually, to run mailcow.\e[0m" + echo -e "\e[31mExiting...\e[0m" + exit 1 + fi +fi + +export LC_ALL=C +DATE=$(date +%Y-%m-%d_%H_%M_%S) +BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD) + while (($#)); do case "${1}" in --check|-c) @@ -318,19 +344,33 @@ while (($#)); do --no-update-compose) NO_UPDATE_COMPOSE=y ;; + --update-compose) + LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php) + COMPOSE_VERSION=$(docker-compose version --short) + if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then + echo -e "\e[33mA new docker-compose Version is available: $LATEST_COMPOSE\e[0m" + echo -e "\e[33mYour Version is: $COMPOSE_VERSION\e[0m" + update_compose + echo -e "\e[32mYour docker-compose Version is now up to date!\e[0m" + else + echo -e "\e[32mYour docker-compose Version is up to date! Not updating it...\e[0m" + fi + exit 0 + ;; --skip-ping-check) SKIP_PING_CHECK=y ;; --help|-h) - echo './update.sh [-c|--check, --ours, --gc, --no-update-compose, --prefetch, --skip-start, --skip-ping-check, -f|--force, -h|--help] + echo './update.sh [-c|--check, --ours, --gc, --no-update-compose, --update-compose, --prefetch, --skip-start, --skip-ping-check, -f|--force, -h|--help] -c|--check - Check for updates and exit (exit codes => 0: update available, 3: no updates) --ours - Use merge strategy option "ours" to solve conflicts in favor of non-mailcow code (local changes over remote changes), not recommended! --gc - Run garbage collector to delete old image tags - --no-update-compose - Do not update docker-compose + --no-update-compose - Skip the docker-compose Updates during the mailcow Update process + --update-compose - Only run the docker-compose Update process (don´t updates your mailcow itself) --prefetch - Only prefetch new images and exit (useful to prepare updates) --skip-start - Do not start mailcow after update - --skip-ping-check - Skip ICMP Check to public DNS resolvers (Use it only if you´ve blocked any ICMP Connections to your mailcow machine). + --skip-ping-check - Skip ICMP Check to public DNS resolvers (Use it only if you´ve blocked any ICMP Connections to your mailcow machine) -f|--force - Force update, do not ask questions ' exit 1 @@ -657,7 +697,15 @@ if [ ! $FORCE ]; then migrate_docker_nat fi -update_compose +LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php) +COMPOSE_VERSION=$(docker-compose version --short) +if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then + echo -e "\e[33mA new docker-compose Version is available: $LATEST_COMPOSE\e[0m" + echo -e "\e[33mYour Version is: $COMPOSE_VERSION\e[0m" + update_compose +else + echo -e "\e[32mYour docker-compose Version is up to date! Not updating it...\e[0m" +fi remove_obsolete_nginx_ports