[Web] Add multiple DKIM at once (+ button to auto-fill missing keys)

[Web] Duplicate DKIM keys from a single domain to a single or multiple domains
[Web] WIP: Started Ratelimit rework
[Web] Show RL in overview of mailbox and domains
[Web] Move RL function out of mailbox functions file
[Web] Some language fixes/changes
This commit is contained in:
André
2018-08-21 17:41:04 +02:00
parent bbbdb13e6a
commit d64a89473e
15 changed files with 508 additions and 231 deletions

View File

@@ -15,61 +15,49 @@ function dkim($_action, $_data = null) {
}
$key_length = intval($_data['key_size']);
$dkim_selector = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : 'dkim';
$domain = $_data['domain'];
if (!is_valid_domain_name($domain) || !is_numeric($key_length)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'dkim_domain_or_sel_invalid'
);
return false;
}
if ($redis->hGet('DKIM_PUB_KEYS', $domain)) {
$domains = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['domains']));
$domains = array_filter($domains);
foreach ($domains as $domain) {
if (!is_valid_domain_name($domain) || !is_numeric($key_length)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'dkim_domain_or_sel_invalid'
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
return false;
}
if (!ctype_alnum($dkim_selector)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'dkim_domain_or_sel_invalid'
);
return false;
}
$config = array(
"digest_alg" => "sha256",
"private_key_bits" => $key_length,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
);
if ($keypair_ressource = openssl_pkey_new($config)) {
$key_details = openssl_pkey_get_details($keypair_ressource);
$pubKey = implode(array_slice(
array_filter(
explode(PHP_EOL, $key_details['key'])
), 1, -1)
);
// Save public key and selector to redis
try {
$redis->hSet('DKIM_PUB_KEYS', $domain, $pubKey);
$redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
continue;
}
catch (RedisException $e) {
if ($redis->hGet('DKIM_PUB_KEYS', $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
return false;
continue;
}
// Export private key and save private key to redis
openssl_pkey_export($keypair_ressource, $privKey);
if (isset($privKey) && !empty($privKey)) {
if (!ctype_alnum($dkim_selector)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
continue;
}
$config = array(
"digest_alg" => "sha256",
"private_key_bits" => $key_length,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
);
if ($keypair_ressource = openssl_pkey_new($config)) {
$key_details = openssl_pkey_get_details($keypair_ressource);
$pubKey = implode(array_slice(
array_filter(
explode(PHP_EOL, $key_details['key'])
), 1, -1)
);
// Save public key and selector to redis
try {
$redis->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, trim($privKey));
$redis->hSet('DKIM_PUB_KEYS', $domain, $pubKey);
$redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
@@ -77,23 +65,79 @@ function dkim($_action, $_data = null) {
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
);
return false;
continue;
}
// Export private key and save private key to redis
openssl_pkey_export($keypair_ressource, $privKey);
if (isset($privKey) && !empty($privKey)) {
try {
$redis->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, trim($privKey));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
);
continue;
}
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_added', $domain)
);
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
continue;
}
}
break;
case 'duplicate':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'access_denied'
);
return false;
}
$from_domain = $_data['from_domain'];
$from_domain_dkim = dkim('details', $from_domain);
if (empty($from_domain_dkim)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('dkim_domain_or_sel_invalid', $from_domain)
);
continue;
}
$to_domains = (array)$_data['to_domain'];
$to_domains = array_filter($to_domains);
foreach ($to_domains as $to_domain) {
try {
$redis->hSet('DKIM_PUB_KEYS', $to_domain, $from_domain_dkim['pubkey']);
$redis->hSet('DKIM_SELECTORS', $to_domain, $from_domain_dkim['dkim_selector']);
$redis->hSet('DKIM_PRIV_KEYS', $from_domain_dkim['dkim_selector'] . '.' . $to_domain, trim($from_domain_dkim['privkey']));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e)
);
continue;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'dkim_added'
'msg' => array('dkim_duplicated', $from_domain, $to_domain)
);
return true;
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'dkim_domain_or_sel_invalid'
);
return false;
}
break;
case 'import':
@@ -129,7 +173,7 @@ function dkim($_action, $_data = null) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'dkim_domain_or_sel_invalid'
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
return false;
}
@@ -137,7 +181,7 @@ function dkim($_action, $_data = null) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'dkim_domain_or_sel_invalid'
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
return false;
}
@@ -145,7 +189,7 @@ function dkim($_action, $_data = null) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'dkim_domain_or_sel_invalid'
'msg' => array('dkim_domain_or_sel_invalid', $domain)
);
return false;
}
@@ -178,7 +222,7 @@ function dkim($_action, $_data = null) {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'dkim_added'
'msg' => array('dkim_added', $domain)
);
return true;
break;
@@ -203,13 +247,7 @@ function dkim($_action, $_data = null) {
}
$dkimdata['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data;
$dkimdata['dkim_selector'] = $redis->hGet('DKIM_SELECTORS', $_data);
$dkimdata['privkey'] = $redis->hGet('DKIM_PRIV_KEYS', $dkimdata['dkim_selector'] . $_data);
if ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] === true) {
$dkimdata['privkey'] = base64_encode($redis->hGet('DKIM_PRIV_KEYS', $dkimdata['dkim_selector'] . '.' . $_data));
}
else {
$dkimdata['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.');
}
$dkimdata['privkey'] = base64_encode($redis->hGet('DKIM_PRIV_KEYS', $dkimdata['dkim_selector'] . '.' . $_data));
}
return $dkimdata;
break;

View File

@@ -1291,86 +1291,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
}
break;
case 'ratelimit':
if (!is_array($_data['object'])) {
$objects = array();
$objects[] = $_data['object'];
}
else {
$objects = $_data['object'];
}
foreach ($objects as $object) {
$rl_value = intval($_data['rl_value']);
$rl_frame = $_data['rl_frame'];
if (!in_array($rl_frame, array('s', 'm', 'h'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'rl_timeframe'
);
continue;
}
if (is_valid_domain_name($object)) {
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
}
elseif (filter_var($object, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (empty($rl_value)) {
try {
$redis->hDel('RL_VALUE', $object);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
continue;
}
}
else {
try {
$redis->hSet('RL_VALUE', $object, $rl_value . ' / 1' . $rl_frame);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
continue;
}
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('object_modified', $object)
);
}
break;
case 'syncjob':
if (!is_array($_data['id'])) {
$ids = array();
@@ -2670,51 +2590,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
return $aliases;
break;
case 'ratelimit':
if (is_valid_domain_name($_data)) {
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
}
elseif (filter_var($_data, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
}
else {
return false;
}
try {
if ($rl_value = $redis->hGet('RL_VALUE', $_data)) {
$rl = explode(' / 1', $rl_value);
$data['value'] = $rl[0];
$data['frame'] = $rl[1];
return $data;
}
else {
return false;
}
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
return false;
}
return false;
break;
case 'alias_details':
$aliasdata = array();
$stmt = $pdo->prepare("SELECT
@@ -2759,6 +2634,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
break;
case 'alias_domain_details':
$aliasdomaindata = array();
$rl = ratelimit('get', 'domain', $_data);
$stmt = $pdo->prepare("SELECT
`alias_domain`,
`target_domain`,
@@ -2775,6 +2651,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$aliasdomaindata['alias_domain'] = $row['alias_domain'];
$aliasdomaindata['target_domain'] = $row['target_domain'];
$aliasdomaindata['active'] = $row['active'];
$aliasdomaindata['rl'] = $rl;
$aliasdomaindata['active_int'] = $row['active_int'];
$aliasdomaindata['created'] = $row['created'];
$aliasdomaindata['modified'] = $row['modified'];
@@ -2848,6 +2725,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
AND `domain` = :domain");
$stmt->execute(array(':domain' => $row['domain']));
$MailboxDataDomain = $stmt->fetch(PDO::FETCH_ASSOC);
$rl = ratelimit('get', 'domain', $_data);
$domaindata['max_new_mailbox_quota'] = ($row['quota'] * 1048576) - $MailboxDataDomain['in_use'];
if ($domaindata['max_new_mailbox_quota'] > ($row['maxquota'] * 1048576)) {
$domaindata['max_new_mailbox_quota'] = ($row['maxquota'] * 1048576);
@@ -2864,6 +2742,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$domaindata['relayhost'] = $row['relayhost'];
$domaindata['backupmx'] = $row['backupmx'];
$domaindata['backupmx_int'] = $row['backupmx_int'];
$domaindata['rl'] = $rl;
$domaindata['active'] = $row['active'];
$domaindata['active_int'] = $row['active_int'];
$domaindata['relay_all_recipients'] = $row['relay_all_recipients'];
@@ -2887,6 +2766,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return false;
}
$mailboxdata = array();
$rl = ratelimit('get', 'mailbox', $_data);
$stmt = $pdo->prepare("SELECT
`domain`.`backupmx`,
`mailbox`.`username`,
@@ -2918,6 +2798,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$mailboxdata['max_new_quota'] = ($DomainQuota['maxquota'] * 1048576);
}
$mailboxdata['username'] = $row['username'];
$mailboxdata['rl'] = $rl;
$mailboxdata['is_relayed'] = $row['backupmx'];
$mailboxdata['name'] = $row['name'];
$mailboxdata['active'] = $row['active'];

View File

@@ -0,0 +1,188 @@
<?php
function ratelimit($_action, $_scope, $_data = null) {
global $redis;
global $lang;
$_data_log = $_data;
switch ($_action) {
case 'edit':
switch ($_scope) {
case 'domain':
if (!is_array($_data['object'])) {
$objects = array();
$objects[] = $_data['object'];
}
else {
$objects = $_data['object'];
}
foreach ($objects as $object) {
$rl_value = intval($_data['rl_value']);
$rl_frame = $_data['rl_frame'];
if (!in_array($rl_frame, array('s', 'm', 'h'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => 'rl_timeframe'
);
continue;
}
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => 'access_denied'
);
continue;
}
if (empty($rl_value)) {
try {
$redis->hDel('RL_VALUE', $object);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e)
);
continue;
}
}
else {
try {
$redis->hSet('RL_VALUE', $object, $rl_value . ' / 1' . $rl_frame);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e)
);
continue;
}
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('object_modified', $object)
);
}
break;
case 'mailbox':
if (!is_array($_data['object'])) {
$objects = array();
$objects[] = $_data['object'];
}
else {
$objects = $_data['object'];
}
foreach ($objects as $object) {
$rl_value = intval($_data['rl_value']);
$rl_frame = $_data['rl_frame'];
if (!in_array($rl_frame, array('s', 'm', 'h'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => 'rl_timeframe'
);
continue;
}
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)
|| ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin')) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => 'access_denied'
);
continue;
}
if (empty($rl_value)) {
try {
$redis->hDel('RL_VALUE', $object);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e)
);
continue;
}
}
else {
try {
$redis->hSet('RL_VALUE', $object, $rl_value . ' / 1' . $rl_frame);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e)
);
continue;
}
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('object_modified', $object)
);
}
break;
}
break;
case 'get':
switch ($_scope) {
case 'domain':
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
try {
if ($rl_value = $redis->hGet('RL_VALUE', $_data)) {
$rl = explode(' / 1', $rl_value);
$data['value'] = $rl[0];
$data['frame'] = $rl[1];
return $data;
}
else {
return false;
}
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e)
);
return false;
}
return false;
break;
case 'mailbox':
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)
|| ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin')) {
return false;
}
try {
if ($rl_value = $redis->hGet('RL_VALUE', $_data)) {
$rl = explode(' / 1', $rl_value);
$data['value'] = $rl[0];
$data['frame'] = $rl[1];
return $data;
}
else {
return false;
}
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e)
);
return false;
}
return false;
break;
}
break;
}
}

View File

@@ -132,6 +132,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.quarantine.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.relayhost.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.rsettings.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';