first
This commit is contained in:
commit
5aa7d034f7
3292 changed files with 465160 additions and 0 deletions
59
data/web/inc/ajax/container_ctrl.php
Executable file
59
data/web/inc/ajax/container_ctrl.php
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
// Block requests by checking the 'Sec-Fetch-Dest' header.
|
||||
if (isset($_SERVER['HTTP_SEC_FETCH_DEST']) && $_SERVER['HTTP_SEC_FETCH_DEST'] !== 'empty') {
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
|
||||
exit();
|
||||
}
|
||||
|
||||
if (preg_match('/^[a-z\-]{0,}-mailcow/', $_GET['service'])) {
|
||||
if ($_GET['action'] == "start") {
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
$retry = 0;
|
||||
while (docker('info', $_GET['service'])['State']['Running'] != 1 && $retry <= 3) {
|
||||
$response = docker('post', $_GET['service'], 'start');
|
||||
$response = json_decode($response, true);
|
||||
$last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
|
||||
if ($response['type'] == "success") {
|
||||
break;
|
||||
}
|
||||
usleep(1500000);
|
||||
$retry++;
|
||||
}
|
||||
echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Already running</span></b>' : $last_response;
|
||||
}
|
||||
if ($_GET['action'] == "stop") {
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
$retry = 0;
|
||||
while (docker('info', $_GET['service'])['State']['Running'] == 1 && $retry <= 3) {
|
||||
$response = docker('post', $_GET['service'], 'stop');
|
||||
$response = json_decode($response, true);
|
||||
$last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
|
||||
if ($response['type'] == "success") {
|
||||
break;
|
||||
}
|
||||
usleep(1500000);
|
||||
$retry++;
|
||||
}
|
||||
echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Not running</span></b>' : $last_response;
|
||||
}
|
||||
if ($_GET['action'] == "restart") {
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
$response = docker('post', $_GET['service'], 'restart');
|
||||
$response = json_decode($response, true);
|
||||
$last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
|
||||
echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Cannot restart container</span></b>' : $last_response;
|
||||
}
|
||||
if ($_GET['action'] == "logs") {
|
||||
$lines = (empty($_GET['lines']) || !is_numeric($_GET['lines'])) ? 1000 : $_GET['lines'];
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
print_r(preg_split('/\n/', docker('logs', $_GET['service'], $lines)));
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
6
data/web/inc/ajax/destroy_tfa_auth.php
Executable file
6
data/web/inc/ajax/destroy_tfa_auth.php
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
session_start();
|
||||
unset($_SESSION['pending_mailcow_cc_username']);
|
||||
unset($_SESSION['pending_mailcow_cc_role']);
|
||||
unset($_SESSION['pending_tfa_methods']);
|
||||
?>
|
||||
458
data/web/inc/ajax/dns_diagnostics.php
Executable file
458
data/web/inc/ajax/dns_diagnostics.php
Executable file
|
|
@ -0,0 +1,458 @@
|
|||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/spf.inc.php';
|
||||
|
||||
define('state_good', '<i class="bi bi-check-lg text-success"></i>');
|
||||
define('state_missing', '<i class="bi bi-x-lg text-danger"></i>');
|
||||
define('state_nomatch', "?");
|
||||
define('state_optional', " <sup>2</sup>");
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin"|| $_SESSION['mailcow_cc_role'] == "domainadmin")) {
|
||||
|
||||
$alias_domains = array();
|
||||
|
||||
if (isset($_GET['domain'])) {
|
||||
$domain_details = mailbox('get', 'domain_details', $_GET['domain']);
|
||||
if ($domain_details !== false) {
|
||||
$domain = $_GET['domain'];
|
||||
$alias_domains = array_merge($alias_domains, mailbox('get', 'alias_domains', $domain));
|
||||
}
|
||||
else {
|
||||
echo "No such domain in context";
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
$ch = curl_init('http://ip4.mailcow.email');
|
||||
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, false);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
|
||||
$ip = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$ch = curl_init('http://ip6.mailcow.email');
|
||||
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
|
||||
curl_setopt($ch, CURLOPT_VERBOSE, false);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
|
||||
$ip6 = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$ptr = implode('.', array_reverse(explode('.', $ip))) . '.in-addr.arpa';
|
||||
if (!empty($ip6)) {
|
||||
$ip6_full = str_replace('::', str_repeat(':', 9-substr_count($ip6, ':')), $ip6);
|
||||
$ip6_full = str_replace('::', ':0:', $ip6_full);
|
||||
$ip6_full = str_replace('::', ':0:', $ip6_full);
|
||||
$ptr6 = '';
|
||||
foreach (explode(':', $ip6_full) as $part) {
|
||||
$ptr6 .= str_pad($part, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
$ptr6 = implode('.', array_reverse(str_split($ptr6, 1))) . '.ip6.arpa';
|
||||
}
|
||||
|
||||
$https_port = strpos($_SERVER['HTTP_HOST'], ':');
|
||||
if ($https_port === FALSE) {
|
||||
$https_port = 443;
|
||||
}
|
||||
else {
|
||||
$https_port = substr($_SERVER['HTTP_HOST'], $https_port+1);
|
||||
}
|
||||
|
||||
if (!isset($autodiscover_config['sieve'])) {
|
||||
$autodiscover_config['sieve'] = array(
|
||||
'server' => $mailcow_hostname,
|
||||
'port' => array_pop(explode(':', getenv('SIEVE_PORT')))
|
||||
);
|
||||
}
|
||||
|
||||
// Init records array
|
||||
$spf_link = '<a href="http://www.open-spf.org/SPF_Record_Syntax/" target="_blank">SPF Record Syntax</a><br />';
|
||||
$dmarc_link = '<a href="https://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>';
|
||||
|
||||
$records = array();
|
||||
|
||||
if ($_SESSION['mailcow_cc_role'] == "admin") {
|
||||
$records[] = array(
|
||||
$mailcow_hostname,
|
||||
'A',
|
||||
$ip
|
||||
);
|
||||
$records[] = array(
|
||||
$ptr,
|
||||
'PTR',
|
||||
$mailcow_hostname
|
||||
);
|
||||
if (!empty($ip6)) {
|
||||
$records[] = array(
|
||||
$mailcow_hostname,
|
||||
'AAAA',
|
||||
expand_ipv6($ip6)
|
||||
);
|
||||
$records[] = array(
|
||||
$ptr6,
|
||||
'PTR',
|
||||
$mailcow_hostname
|
||||
);
|
||||
}
|
||||
$records[] = array(
|
||||
'_25._tcp.' . $autodiscover_config['smtp']['server'],
|
||||
'TLSA',
|
||||
generate_tlsa_digest($autodiscover_config['smtp']['server'], 25, 1)
|
||||
);
|
||||
}
|
||||
|
||||
$records[] = array(
|
||||
$domain,
|
||||
'MX',
|
||||
$mailcow_hostname
|
||||
);
|
||||
|
||||
if (!in_array($domain, $alias_domains)) {
|
||||
$records[] = array(
|
||||
'autodiscover.' . $domain,
|
||||
'CNAME',
|
||||
$mailcow_hostname
|
||||
);
|
||||
$records[] = array(
|
||||
'_autodiscover._tcp.' . $domain,
|
||||
'SRV',
|
||||
$mailcow_hostname . ' ' . $https_port
|
||||
);
|
||||
$records[] = array(
|
||||
'autoconfig.' . $domain,
|
||||
'CNAME',
|
||||
$mailcow_hostname
|
||||
);
|
||||
}
|
||||
|
||||
$records[] = array(
|
||||
$domain,
|
||||
'TXT',
|
||||
$spf_link,
|
||||
state_optional
|
||||
);
|
||||
|
||||
$records[] = array(
|
||||
'_dmarc.' . $domain,
|
||||
'TXT',
|
||||
$dmarc_link,
|
||||
state_optional
|
||||
);
|
||||
|
||||
if (!empty($dkim = dkim('details', $domain))) {
|
||||
$records[] = array(
|
||||
$dkim['dkim_selector'] . '._domainkey.' . $domain,
|
||||
'TXT',
|
||||
$dkim['dkim_txt']
|
||||
);
|
||||
}
|
||||
|
||||
if (!in_array($domain, $alias_domains)) {
|
||||
$current_records = (array)dns_get_record('_pop3._tcp.' . $domain, DNS_SRV);
|
||||
if (count($current_records) == 0 || $current_records[0]['target'] != '') {
|
||||
if ($autodiscover_config['pop3']['tlsport'] != '110') {
|
||||
$records[] = array(
|
||||
'_pop3._tcp.' . $domain,
|
||||
'SRV',
|
||||
$autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['tlsport']
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$records[] = array(
|
||||
'_pop3._tcp.' . $domain,
|
||||
'SRV',
|
||||
'. 0'
|
||||
);
|
||||
}
|
||||
|
||||
$current_records = (array)dns_get_record('_pop3s._tcp.' . $domain, DNS_SRV);
|
||||
|
||||
if (count($current_records) == 0 || $current_records[0]['target'] != '') {
|
||||
if ($autodiscover_config['pop3']['port'] != '995') {
|
||||
$records[] = array(
|
||||
'_pop3s._tcp.' . $domain,
|
||||
'SRV',
|
||||
$autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['port']
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$records[] = array(
|
||||
'_pop3s._tcp.' . $domain,
|
||||
'SRV',
|
||||
'. 0'
|
||||
);
|
||||
}
|
||||
|
||||
if ($autodiscover_config['imap']['tlsport'] != '143') {
|
||||
$records[] = array(
|
||||
'_imap._tcp.' . $domain,
|
||||
'SRV',
|
||||
$autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['tlsport']
|
||||
);
|
||||
}
|
||||
|
||||
if ($autodiscover_config['imap']['port'] != '993') {
|
||||
$records[] = array(
|
||||
'_imaps._tcp.' . $domain,
|
||||
'SRV',
|
||||
$autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['port']
|
||||
);
|
||||
}
|
||||
|
||||
if ($autodiscover_config['smtp']['tlsport'] != '587') {
|
||||
$records[] = array(
|
||||
'_submission._tcp.' . $domain,
|
||||
'SRV',
|
||||
$autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['tlsport']
|
||||
);
|
||||
}
|
||||
|
||||
if ($autodiscover_config['smtp']['port'] != '465') {
|
||||
$records[] = array(
|
||||
'_smtps._tcp.' . $domain,
|
||||
'SRV',
|
||||
$autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['port']
|
||||
);
|
||||
}
|
||||
|
||||
if ($autodiscover_config['sieve']['port'] != '4190') {
|
||||
$records[] = array(
|
||||
'_sieve._tcp.' . $domain,
|
||||
'SRV',
|
||||
$autodiscover_config['sieve']['server'] . ' ' . $autodiscover_config['sieve']['port']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$record_types = array(
|
||||
'A' => DNS_A,
|
||||
'AAAA' => DNS_AAAA,
|
||||
'CNAME' => DNS_CNAME,
|
||||
'MX' => DNS_MX,
|
||||
'PTR' => DNS_PTR,
|
||||
'SRV' => DNS_SRV,
|
||||
'TXT' => DNS_TXT,
|
||||
);
|
||||
|
||||
$data_field = array(
|
||||
'A' => 'ip',
|
||||
'AAAA' => 'ipv6',
|
||||
'CNAME' => 'target',
|
||||
'MX' => 'target',
|
||||
'PTR' => 'target',
|
||||
'SRV' => 'data',
|
||||
'TLSA' => 'data',
|
||||
'TXT' => 'txt',
|
||||
);
|
||||
|
||||
?>
|
||||
<div class="table-responsive" id="dnstable">
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th><?=$lang['diagnostics']['dns_records_name'];?></th>
|
||||
<th><?=$lang['diagnostics']['dns_records_type'];?></th>
|
||||
<th><?=$lang['diagnostics']['dns_records_data'];?></th>
|
||||
<th><?=$lang['diagnostics']['dns_records_status'];?></th>
|
||||
</tr>
|
||||
<?php
|
||||
foreach ($records as &$record) {
|
||||
$record[1] = strtoupper($record[1]);
|
||||
$state = state_optional;
|
||||
|
||||
if ($record[1] == 'TLSA') {
|
||||
$currents = (array)dns_get_record($record[0], 52, $_, $_, TRUE);
|
||||
foreach ($currents as &$current) {
|
||||
$current['type'] = 'TLSA';
|
||||
$current['cert_usage'] = hexdec(bin2hex($current['data'][0]));
|
||||
$current['selector'] = hexdec(bin2hex($current['data'][1]));
|
||||
$current['match_type'] = hexdec(bin2hex($current['data'][2]));
|
||||
$current['cert_data'] = bin2hex(substr($current['data'], 3));
|
||||
$current['data'] = $current['cert_usage'] . ' ' . $current['selector'] . ' ' . $current['match_type'] . ' ' . $current['cert_data'];
|
||||
}
|
||||
unset($current);
|
||||
}
|
||||
else {
|
||||
$currents = (array)dns_get_record($record[0], $record_types[$record[1]]);
|
||||
if ($record[0] == $mailcow_hostname && ($record[1] == "A" || $record[1] == "AAAA")) {
|
||||
if (!empty((array)dns_get_record($record[0], DNS_CNAME))) {
|
||||
$currents[0]['ip'] = state_missing . ' <b>(CNAME)</b>';
|
||||
$currents[0]['ipv6'] = state_missing . ' <b>(CNAME)</b>';
|
||||
}
|
||||
}
|
||||
if ($record[1] == 'SRV') {
|
||||
foreach ($currents as &$current) {
|
||||
if ($current['target'] == '') {
|
||||
$current['target'] = '.';
|
||||
$current['port'] = '0';
|
||||
}
|
||||
$current['data'] = $current['target'] . ' ' . $current['port'];
|
||||
}
|
||||
unset($current);
|
||||
}
|
||||
elseif ($record[1] == 'TXT') {
|
||||
foreach ($currents as &$current) {
|
||||
unset($current);
|
||||
}
|
||||
unset($current);
|
||||
}
|
||||
elseif ($record[1] == 'AAAA') {
|
||||
foreach ($currents as &$current) {
|
||||
$current['ipv6'] = expand_ipv6($current['ipv6']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($record[1] == 'CNAME' && count($currents) == 0) {
|
||||
// A and AAAA are also valid instead of CNAME
|
||||
$a = (array)dns_get_record($record[0], DNS_A);
|
||||
$cname = (array)dns_get_record($record[2], DNS_A);
|
||||
if (count($a) > 0 && count($cname) > 0) {
|
||||
if ($a[0]['ip'] == $cname[0]['ip']) {
|
||||
$currents = array(
|
||||
array(
|
||||
'host' => $record[0],
|
||||
'class' => 'IN',
|
||||
'type' => 'CNAME',
|
||||
'target' => $record[2]
|
||||
)
|
||||
);
|
||||
$aaaa = (array)dns_get_record($record[0], DNS_AAAA);
|
||||
$cname = (array)dns_get_record($record[2], DNS_AAAA);
|
||||
if (count($aaaa) == 0 || count($cname) == 0 || expand_ipv6($aaaa[0]['ipv6']) != expand_ipv6($cname[0]['ipv6'])) {
|
||||
$currents[0]['target'] = expand_ipv6($aaaa[0]['ipv6']) . ' <sup>1</sup>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$currents = array(
|
||||
array(
|
||||
'host' => $record[0],
|
||||
'class' => 'IN',
|
||||
'type' => 'CNAME',
|
||||
'target' => $a[0]['ip'] . ' <sup>1</sup>'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($currents as &$current) {
|
||||
if ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=dmarc') === 0 &&
|
||||
$record[2] == $dmarc_link) {
|
||||
$current['txt'] = str_replace(' ', '', $current['txt']);
|
||||
$state = $current[$data_field[$current['type']]] . state_optional;
|
||||
}
|
||||
elseif ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=spf') === 0 &&
|
||||
$record[2] == $spf_link) {
|
||||
$state = state_nomatch;
|
||||
$rslt = get_spf_allowed_hosts($record[0], true);
|
||||
if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) {
|
||||
$state = state_good;
|
||||
}
|
||||
$state .= '<br />' . $current[$data_field[$current['type']]] . state_optional;
|
||||
}
|
||||
elseif ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=dkim') === 0 &&
|
||||
stripos($record[2], 'v=dkim') === 0) {
|
||||
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $current[$data_field[$current['type']]], $dkim_matches_current);
|
||||
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $record[2], $dkim_matches_good);
|
||||
if ($dkim_matches_current[1] == $dkim_matches_good[1]) {
|
||||
$state = state_good;
|
||||
}
|
||||
}
|
||||
elseif ($current['type'] != 'TXT' &&
|
||||
isset($data_field[$current['type']]) && $state != state_good) {
|
||||
$state = state_nomatch;
|
||||
if ($current[$data_field[$current['type']]] == $record[2]) {
|
||||
$state = state_good;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($current);
|
||||
|
||||
if (isset($record[3]) &&
|
||||
$record[3] == state_optional &&
|
||||
($state == state_missing || $state == state_nomatch)) {
|
||||
$state = state_optional;
|
||||
}
|
||||
|
||||
if ($state == state_nomatch) {
|
||||
$state = array();
|
||||
foreach ($currents as $current) {
|
||||
$state[] = $current[$data_field[$current['type']]];
|
||||
}
|
||||
$state = implode('<br />', $state);
|
||||
}
|
||||
echo sprintf('
|
||||
<tr>
|
||||
<td>%s</td>
|
||||
<td>%s</td>
|
||||
<td class="dns-found">%s</td>
|
||||
<td class="dns-recommended">%s</td>
|
||||
</tr>', $record[0], $record[1], $record[2], $state);
|
||||
$record[3] = explode('<br />', $state);
|
||||
}
|
||||
|
||||
unset($record);
|
||||
|
||||
$dns_data = sprintf("\$ORIGIN %s.\n", $domain);
|
||||
foreach ($records as $record) {
|
||||
if ($domain == substr($record[0], -strlen($domain))) {
|
||||
$label = substr($record[0], 0, -strlen($domain)-1);
|
||||
$val = $record[2];
|
||||
|
||||
if (strlen($label) == 0) {
|
||||
$label = "@";
|
||||
}
|
||||
|
||||
$vals = array();
|
||||
if (strpos($val, "<a") !== FALSE) {
|
||||
if (is_array($record[3]) && count($record[3]) == 1 && $record[3][0] == state_optional) {
|
||||
$record[3][0] = "**TODO**";
|
||||
$label = ';' . $label;
|
||||
}
|
||||
foreach ($record[3] as $val) {
|
||||
$val = str_replace(state_optional, '', $val);
|
||||
$val = str_replace(state_good, '', $val);
|
||||
if (strlen($val) > 0) {
|
||||
$vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
|
||||
}
|
||||
|
||||
foreach ($vals as $val) {
|
||||
$dns_data .= str_replace($domain, $domain . '.', $val);
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</table>
|
||||
<a id='download-zonefile' class="btn btn-sm btn-secondary visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline mb-4" style="margin-top:10px" data-zonefile="<?=base64_encode($dns_data);?>" download='<?=$_GET['domain'];?>.txt' type='text/csv'>Download</a>
|
||||
<script>
|
||||
var zonefile_dl_link = document.getElementById('download-zonefile');
|
||||
var zonefile = atob(zonefile_dl_link.getAttribute('data-zonefile'));
|
||||
var data = new Blob([zonefile]);
|
||||
var download_zonefile_link = document.getElementById('download-zonefile');
|
||||
download_zonefile_link.href = URL.createObjectURL(data);
|
||||
</script>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
<sup>1</sup> <?=$lang['diagnostics']['cname_from_a'];?><br />
|
||||
<sup>2</sup> <?=$lang['diagnostics']['optional'];?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
else {
|
||||
echo "Session invalid";
|
||||
exit();
|
||||
}
|
||||
?>
|
||||
207
data/web/inc/ajax/qitem_details.php
Executable file
207
data/web/inc/ajax/qitem_details.php
Executable file
|
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
header("Content-Type: application/json");
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
|
||||
function rrmdir($src) {
|
||||
$dir = opendir($src);
|
||||
while(false !== ( $file = readdir($dir)) ) {
|
||||
if (( $file != '.' ) && ( $file != '..' )) {
|
||||
$full = $src . '/' . $file;
|
||||
if ( is_dir($full) ) {
|
||||
rrmdir($full);
|
||||
}
|
||||
else {
|
||||
unlink($full);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($dir);
|
||||
rmdir($src);
|
||||
}
|
||||
|
||||
function addAddresses(&$list, $mail, $headerName) {
|
||||
$addresses = $mail->getAddresses($headerName);
|
||||
foreach ($addresses as $address) {
|
||||
if (filter_var($address['address'], FILTER_VALIDATE_EMAIL)) {
|
||||
$list[] = array('address' => $address['address'], 'type' => $headerName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($_GET['hash']) && ctype_alnum($_GET['hash'])) {
|
||||
$mailc = quarantine('hash_details', $_GET['hash']);
|
||||
if ($mailc === false) {
|
||||
echo json_encode(array('error' => 'Message invalid'));
|
||||
exit;
|
||||
}
|
||||
if (strlen($mailc['msg']) > 10485760) {
|
||||
echo json_encode(array('error' => 'Message size exceeds 10 MiB.'));
|
||||
exit;
|
||||
}
|
||||
if (!empty($mailc['msg'])) {
|
||||
// Init message array
|
||||
$data = array();
|
||||
// Init parser
|
||||
$mail_parser = new PhpMimeMailParser\Parser();
|
||||
$html2text = new Html2Text\Html2Text();
|
||||
// Load msg to parser
|
||||
$mail_parser->setText($mailc['msg']);
|
||||
// Get mail recipients
|
||||
{
|
||||
$recipientsList = array();
|
||||
addAddresses($recipientsList, $mail_parser, 'to');
|
||||
addAddresses($recipientsList, $mail_parser, 'cc');
|
||||
addAddresses($recipientsList, $mail_parser, 'bcc');
|
||||
$recipientsList[] = array('address' => $mailc['rcpt'], 'type' => 'smtp');
|
||||
$data['recipients'] = $recipientsList;
|
||||
}
|
||||
// Get from
|
||||
$data['header_from'] = $mail_parser->getHeader('from');
|
||||
$data['env_from'] = $mailc['sender'];
|
||||
// Get rspamd score
|
||||
$data['score'] = $mailc['score'];
|
||||
// Get rspamd action
|
||||
$data['action'] = $mailc['action'];
|
||||
// Get rspamd symbols
|
||||
$data['symbols'] = json_decode($mailc['symbols']);
|
||||
// Get fuzzy hashes
|
||||
$data['fuzzy_hashes'] = json_decode($mailc['fuzzy_hashes']);
|
||||
$data['subject'] = mb_convert_encoding($mail_parser->getHeader('subject'), "UTF-8", "auto");
|
||||
(empty($data['subject'])) ? $data['subject'] = '-' : null;
|
||||
echo json_encode($data);
|
||||
}
|
||||
}
|
||||
elseif (!empty($_GET['id']) && ctype_alnum($_GET['id'])) {
|
||||
if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||
echo json_encode(array('error' => 'Access denied'));
|
||||
exit();
|
||||
}
|
||||
$tmpdir = '/tmp/' . $_GET['id'] . '/';
|
||||
$mailc = quarantine('details', $_GET['id']);
|
||||
if ($mailc === false) {
|
||||
echo json_encode(array('error' => 'Access denied'));
|
||||
exit;
|
||||
}
|
||||
if (strlen($mailc['msg']) > 10485760) {
|
||||
echo json_encode(array('error' => 'Message size exceeds 10 MiB.'));
|
||||
exit;
|
||||
}
|
||||
if (!empty($mailc['msg'])) {
|
||||
if (isset($_GET['quick_release'])) {
|
||||
$hash = hash('sha256', $mailc['id'] . $mailc['qid']);
|
||||
header('Location: /qhandler/release/' . $hash);
|
||||
exit;
|
||||
}
|
||||
if (isset($_GET['quick_delete'])) {
|
||||
$hash = hash('sha256', $mailc['id'] . $mailc['qid']);
|
||||
header('Location: /qhandler/delete/' . $hash);
|
||||
exit;
|
||||
}
|
||||
// Init message array
|
||||
$data = array();
|
||||
// Init parser
|
||||
$mail_parser = new PhpMimeMailParser\Parser();
|
||||
$html2text = new Html2Text\Html2Text();
|
||||
// Load msg to parser
|
||||
$mail_parser->setText($mailc['msg']);
|
||||
|
||||
// Get mail recipients
|
||||
{
|
||||
$recipientsList = array();
|
||||
addAddresses($recipientsList, $mail_parser, 'to');
|
||||
addAddresses($recipientsList, $mail_parser, 'cc');
|
||||
addAddresses($recipientsList, $mail_parser, 'bcc');
|
||||
$recipientsList[] = array('address' => $mailc['rcpt'], 'type' => 'smtp');
|
||||
$data['recipients'] = $recipientsList;
|
||||
}
|
||||
// Get from
|
||||
$data['header_from'] = $mail_parser->getHeader('from');
|
||||
$data['env_from'] = $mailc['sender'];
|
||||
// Get rspamd score
|
||||
$data['score'] = $mailc['score'];
|
||||
// Get rspamd action
|
||||
$data['action'] = $mailc['action'];
|
||||
// Get rspamd symbols
|
||||
$data['symbols'] = json_decode($mailc['symbols']);
|
||||
// Get fuzzy hashes
|
||||
$data['fuzzy_hashes'] = json_decode($mailc['fuzzy_hashes']);
|
||||
// Get text/plain content
|
||||
$data['text_plain'] = $mail_parser->getMessageBody('text');
|
||||
if (!json_encode($data['text_plain'])) $data['text_plain'] = '';
|
||||
// Get html content and convert to text
|
||||
$data['text_html'] = $html2text->convert($mail_parser->getMessageBody('html'));
|
||||
if (empty($data['text_plain']) && empty($data['text_html'])) {
|
||||
// Failed to parse content, try raw
|
||||
$text = trim(substr($mailc['msg'], strpos($mailc['msg'], "\r\n\r\n") + 1));
|
||||
// Only return html->text
|
||||
$data['text_plain'] = 'Parser failed, assuming HTML';
|
||||
$data['text_html'] = $html2text->convert($text);
|
||||
}
|
||||
(empty($data['text_plain'])) ? $data['text_plain'] = '-' : null;
|
||||
// Get subject
|
||||
$data['subject'] = $mail_parser->getHeader('subject');
|
||||
$data['subject'] = mb_convert_encoding($mail_parser->getHeader('subject'), "UTF-8", "auto");
|
||||
(empty($data['subject'])) ? $data['subject'] = '-' : null;
|
||||
// Get attachments
|
||||
if (is_dir($tmpdir)) {
|
||||
rrmdir($tmpdir);
|
||||
}
|
||||
mkdir('/tmp/' . $_GET['id']);
|
||||
$mail_parser->saveAttachments($tmpdir, true);
|
||||
$atts = $mail_parser->getAttachments(true);
|
||||
if (count($atts) > 0) {
|
||||
foreach ($atts as $key => $val) {
|
||||
$data['attachments'][$key] = array(
|
||||
// Index
|
||||
// 0 => file name
|
||||
// 1 => mime type
|
||||
// 2 => file size
|
||||
// 3 => vt link by sha256
|
||||
$val->getFilename(),
|
||||
$val->getContentType(),
|
||||
filesize($tmpdir . $val->getFilename()),
|
||||
'https://www.virustotal.com/file/' . hash_file('SHA256', $tmpdir . $val->getFilename()) . '/analysis/'
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isset($_GET['eml'])) {
|
||||
$dl_filename = filter_var($data['subject'], FILTER_SANITIZE_STRING);
|
||||
$dl_filename = strlen($dl_filename) > 30 ? substr($dl_filename,0,30) : $dl_filename;
|
||||
header('Pragma: public');
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
|
||||
header('Cache-Control: private', false);
|
||||
header('Content-Type: message/rfc822');
|
||||
header('Content-Disposition: attachment; filename="'. $dl_filename . '.eml";');
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
header('Content-Length: ' . strlen($mailc['msg']));
|
||||
echo $mailc['msg'];
|
||||
exit;
|
||||
}
|
||||
if (isset($_GET['att'])) {
|
||||
if ($_SESSION['acl']['quarantine_attachments'] == 0) {
|
||||
exit(json_encode('Forbidden'));
|
||||
}
|
||||
$dl_id = intval($_GET['att']);
|
||||
$dl_filename = filter_var($data['attachments'][$dl_id][0], FILTER_SANITIZE_STRING);
|
||||
$dl_filename_short = strlen($dl_filename) > 20 ? substr($dl_filename, 0, 20) : $dl_filename;
|
||||
$dl_filename_extension = pathinfo($tmpdir . $dl_filename)['extension'];
|
||||
$dl_filename_short = preg_replace('/\.' . $dl_filename_extension . '$/', '', $dl_filename_short);
|
||||
if (!is_dir($tmpdir . $dl_filename) && file_exists($tmpdir . $dl_filename)) {
|
||||
header('Pragma: public');
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
|
||||
header('Cache-Control: private', false);
|
||||
header('Content-Type: ' . $data['attachments'][$dl_id][1]);
|
||||
header('Content-Disposition: attachment; filename="'. $dl_filename_short . '.' . $dl_filename_extension . '";');
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
header('Content-Length: ' . $data['attachments'][$dl_id][2]);
|
||||
readfile($tmpdir . $dl_filename);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
echo json_encode($data);
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
10
data/web/inc/ajax/qr_gen.php
Executable file
10
data/web/inc/ajax/qr_gen.php
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
header('Content-Type: text/plain');
|
||||
if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||
exit();
|
||||
}
|
||||
if (isset($_GET['token']) && ctype_alnum($_GET['token'])) {
|
||||
echo $tfa->getQRCodeImageAsDataUri($_SESSION['mailcow_cc_username'], $_GET['token']);
|
||||
}
|
||||
?>
|
||||
3
data/web/inc/ajax/show_rspamd_global_filters.php
Executable file
3
data/web/inc/ajax/show_rspamd_global_filters.php
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
session_start();
|
||||
$_SESSION['show_rspamd_global_filters'] = true;
|
||||
22
data/web/inc/ajax/sieve_validation.php
Executable file
22
data/web/inc/ajax/sieve_validation.php
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
header('Content-Type: application/json');
|
||||
if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||
exit();
|
||||
}
|
||||
if (isset($_GET['script'])) {
|
||||
$sieve = new Sieve\SieveParser();
|
||||
try {
|
||||
if (empty($_GET['script'])) {
|
||||
echo json_encode(array('type' => 'danger', 'msg' => $lang['danger']['script_empty']));
|
||||
exit();
|
||||
}
|
||||
$sieve->parse($_GET['script']);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
echo json_encode(array('type' => 'danger', 'msg' => $e->getMessage()));
|
||||
exit();
|
||||
}
|
||||
echo json_encode(array('type' => 'success', 'msg' => $lang['add']['validation_success']));
|
||||
}
|
||||
?>
|
||||
14
data/web/inc/ajax/syncjob_logs.php
Executable file
14
data/web/inc/ajax/syncjob_logs.php
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
header('Content-Type: text/plain');
|
||||
if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||
exit();
|
||||
}
|
||||
|
||||
if (isset($_GET['id']) && is_numeric($_GET['id'])) {
|
||||
if ($details = mailbox('get', 'syncjob_details', intval($_GET['id']))) {
|
||||
echo (empty($details['log'])) ? '-' : $details['log'];
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
149
data/web/inc/ajax/transport_check.php
Executable file
149
data/web/inc/ajax/transport_check.php
Executable file
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php';
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
error_reporting(0);
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
|
||||
$transport_id = intval($_GET['transport_id']);
|
||||
$transport_type = $_GET['transport_type'];
|
||||
if (isset($_GET['mail_from']) && filter_var($_GET['mail_from'], FILTER_VALIDATE_EMAIL)) {
|
||||
$mail_from = $_GET['mail_from'];
|
||||
}
|
||||
else {
|
||||
$mail_from = "relay@example.org";
|
||||
}
|
||||
if (isset($_GET['mail_rcpt']) && filter_var($_GET['mail_rcpt'], FILTER_VALIDATE_EMAIL)) {
|
||||
$mail_rcpt = $_GET['mail_rcpt'];
|
||||
}
|
||||
else {
|
||||
$mail_rcpt = "null@hosted.mailcow.de";
|
||||
}
|
||||
if ($transport_type == 'transport-map') {
|
||||
$transport_details = transport('details', $transport_id);
|
||||
$nexthop = $transport_details['nexthop'];
|
||||
}
|
||||
elseif ($transport_type == 'sender-dependent') {
|
||||
$transport_details = relayhost('details', $transport_id);
|
||||
$nexthop = $transport_details['hostname'];
|
||||
}
|
||||
if (!empty($transport_details)) {
|
||||
// Remove [ and ]
|
||||
$hostname_w_port = preg_replace('/\[|\]/', '', $nexthop);
|
||||
preg_match('/\[.+\](:.+)/', $nexthop, $hostname_port_match);
|
||||
preg_match('/\[\d\.\d\.\d\.\d\](:.+)/', $nexthop, $ipv4_port_match);
|
||||
$has_bracket_and_port = (isset($hostname_port_match[1])) ? true : false;
|
||||
$is_ipv4_and_has_port = (isset($ipv4_port_match[1])) ? true : false;
|
||||
$skip_lookup_mx = strpos($nexthop, '[');
|
||||
// Explode to hostname and port
|
||||
if ($has_bracket_and_port) {
|
||||
$port = substr($hostname_w_port, strrpos($hostname_w_port, ':') + 1);
|
||||
$hostname = preg_replace('/'. preg_quote(':' . $port, '/') . '$/', '', $hostname_w_port);
|
||||
if (filter_var($hostname, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$hostname = '[' . $hostname . ']';
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ($is_ipv4_and_has_port) {
|
||||
$port = substr($hostname_w_port, strrpos($hostname_w_port, ':') + 1);
|
||||
$hostname = preg_replace('/'. preg_quote(':' . $port, '/') . '$/', '', $hostname_w_port);
|
||||
}
|
||||
if (filter_var($hostname_w_port, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$hostname = $hostname_w_port;
|
||||
$port = null;
|
||||
}
|
||||
elseif (filter_var($hostname_w_port, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$hostname = '[' . $hostname_w_port . ']';
|
||||
$port = null;
|
||||
}
|
||||
else {
|
||||
$hostname = preg_replace('/'. preg_quote(':' . $port, '/') . '$/', '', $hostname_w_port);
|
||||
$port = null;
|
||||
}
|
||||
}
|
||||
// Try to get MX if host is not [host]
|
||||
if ($skip_lookup_mx === false) {
|
||||
getmxrr($hostname, $mx_records, $mx_weight);
|
||||
if (!empty($mx_records)) {
|
||||
for ($i = 0; $i < count($mx_records); $i++) {
|
||||
$mxs[$mx_records[$i]] = $mx_weight[$i];
|
||||
}
|
||||
asort ($mxs);
|
||||
$records = array_keys($mxs);
|
||||
echo 'Using first matched primary MX for "' . $hostname . '": ';
|
||||
$hostname = $records[0];
|
||||
echo $hostname . '<br>';
|
||||
}
|
||||
else {
|
||||
echo 'No MX records for ' . $hostname . ' were found in DNS, skipping and using hostname as next-hop.<br>';
|
||||
}
|
||||
}
|
||||
// Use port 25 if no port was given
|
||||
$port = (empty($port)) ? 25 : $port;
|
||||
$username = $transport_details['username'];
|
||||
$password = $transport_details['password'];
|
||||
|
||||
$mail = new PHPMailer;
|
||||
$mail->Timeout = 15;
|
||||
$mail->SMTPOptions = array(
|
||||
'ssl' => array(
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'allow_self_signed' => true
|
||||
)
|
||||
);
|
||||
$mail->SMTPDebug = 3;
|
||||
// smtp: and smtp_enforced_tls: do not support wrapped tls, todo?
|
||||
// change postfix map to detect wrapped tls or add a checkbox to toggle wrapped tls
|
||||
// if ($port == 465) {
|
||||
// $mail->SMTPSecure = "ssl";
|
||||
// }
|
||||
$mail->Debugoutput = function($str, $level) {
|
||||
foreach(preg_split("/((\r?\n)|(\r\n?)|\n)/", $str) as $line){
|
||||
if (empty($line)) { continue; }
|
||||
if (preg_match("/SERVER \-\> CLIENT: 2\d\d.+/i", $line)) {
|
||||
echo '<span style="color:darkgreen;font-weight:bold">' . htmlspecialchars($line) . '</span><br>';
|
||||
}
|
||||
elseif (preg_match("/SERVER \-\> CLIENT: 3\d\d.+/i", $line)) {
|
||||
echo '<span style="color:lightgreen;font-weight:bold">' . htmlspecialchars($line) . '</span><br>';
|
||||
}
|
||||
elseif (preg_match("/SERVER \-\> CLIENT: 4\d\d.+/i", $line)) {
|
||||
echo '<span style="color:yellow;font-weight:bold">' . htmlspecialchars($line) . '</span><br>';
|
||||
}
|
||||
elseif (preg_match("/SERVER \-\> CLIENT: 5\d\d.+/i", $line)) {
|
||||
echo '<span style="color:red;font-weight:bold">' . htmlspecialchars($line) . '</span><br>';
|
||||
}
|
||||
elseif (preg_match("/CLIENT \-\> SERVER:.+/i", $line)) {
|
||||
echo '<span style="color:#999;font-weight:bold">' . htmlspecialchars($line) . '</span><br>';
|
||||
}
|
||||
elseif (preg_match("/^(?!SERVER|CLIENT|Connection:|\)).+$/i", $line)) {
|
||||
echo '<span> ↪ ' . htmlspecialchars($line) . '</span><br>';
|
||||
}
|
||||
else {
|
||||
echo htmlspecialchars($line) . '<br>';
|
||||
}
|
||||
}
|
||||
};
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $hostname;
|
||||
if (!empty($username)) {
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $username;
|
||||
$mail->Password = $password;
|
||||
}
|
||||
$mail->Port = $port;
|
||||
$mail->setFrom($mail_from, 'Mailer');
|
||||
$mail->Subject = 'A subject for a SMTP test';
|
||||
$mail->addAddress($mail_rcpt, 'Joe Null');
|
||||
$mail->Body = 'This is our test body';
|
||||
$mail->send();
|
||||
}
|
||||
else {
|
||||
echo "Unknown transport.";
|
||||
}
|
||||
}
|
||||
else {
|
||||
echo "Permission denied.";
|
||||
}
|
||||
94
data/web/inc/footer.inc.php
Executable file
94
data/web/inc/footer.inc.php
Executable file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
logger();
|
||||
|
||||
$hash = $js_minifier->getDataHash();
|
||||
$JSPath = '/tmp/' . $hash . '.js';
|
||||
if(!file_exists($JSPath)) {
|
||||
$js_minifier->minify($JSPath);
|
||||
cleanupJS($hash);
|
||||
}
|
||||
|
||||
$alertbox_log_parser = alertbox_log_parser($_SESSION);
|
||||
$alerts = [];
|
||||
if (is_array($alertbox_log_parser)) {
|
||||
foreach ($alertbox_log_parser as $log) {
|
||||
$message = htmlspecialchars($log['msg'], ENT_QUOTES);
|
||||
$message = strtr($message, ["\n" => '', "\r" => '', "\t" => '<br>']);
|
||||
$alerts[trim($log['type'], '"')][] = trim($message, '"');
|
||||
}
|
||||
$alert = array_filter(array_unique($alerts));
|
||||
foreach($alert as $alert_type => $alert_msg) {
|
||||
// html breaks from mysql alerts, replace ` with '
|
||||
$alerts[$alert_type] = implode('<hr class="alert-hr">', str_replace("`", "'", $alert_msg));
|
||||
}
|
||||
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(
|
||||
'version_tag' => $GLOBALS['MAILCOW_GIT_VERSION'],
|
||||
'last_version_tag' => $GLOBALS['MAILCOW_LAST_GIT_VERSION'],
|
||||
'git_owner' => $GLOBALS['MAILCOW_GIT_OWNER'],
|
||||
'git_repo' => $GLOBALS['MAILCOW_GIT_REPO'],
|
||||
'git_project_url' => $GLOBALS['MAILCOW_GIT_URL'],
|
||||
'git_commit' => $GLOBALS['MAILCOW_GIT_COMMIT'],
|
||||
'git_commit_date' => $GLOBALS['MAILCOW_GIT_COMMIT_DATE'],
|
||||
'mailcow_branch' => $GLOBALS['MAILCOW_BRANCH'],
|
||||
'updated_at' => $GLOBALS['MAILCOW_UPDATEDAT']
|
||||
),
|
||||
'js_path' => '/cache/'.basename($JSPath),
|
||||
'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']),
|
||||
'lang_tfa' => json_encode($lang['tfa']),
|
||||
'lang_fido2' => json_encode($lang['fido2']),
|
||||
'docker_timeout' => $DOCKER_TIMEOUT,
|
||||
'session_lifetime' => (int)$SESSION_LIFETIME,
|
||||
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
|
||||
'pagination_size' => $PAGINATION_SIZE,
|
||||
'log_pagination_size' => $LOG_PAGINATION_SIZE,
|
||||
'alerts' => $alerts,
|
||||
'totp_secret' => $tfa->createSecret(),
|
||||
];
|
||||
|
||||
foreach ($globalVariables as $globalVariableName => $globalVariableValue) {
|
||||
$twig->addGlobal($globalVariableName, $globalVariableValue);
|
||||
}
|
||||
|
||||
if (is_array($template_data)) {
|
||||
echo $twig->render($template, $template_data);
|
||||
}
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_api'])) {
|
||||
session_regenerate_id(true);
|
||||
session_unset();
|
||||
session_destroy();
|
||||
session_write_close();
|
||||
header("Location: /");
|
||||
}
|
||||
$stmt = null;
|
||||
$pdo = null;
|
||||
226
data/web/inc/functions.acl.inc.php
Executable file
226
data/web/inc/functions.acl.inc.php
Executable file
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
function acl($_action, $_scope = null, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
switch ($_action) {
|
||||
case 'edit':
|
||||
switch ($_scope) {
|
||||
case 'user':
|
||||
if (!is_array($_data['username'])) {
|
||||
$usernames = array();
|
||||
$usernames[] = $_data['username'];
|
||||
}
|
||||
else {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
// Cast to array for single selections
|
||||
$acls = (array)$_data['user_acl'];
|
||||
// Create associative array from index array
|
||||
// All set items are given 1 as value
|
||||
foreach ($acls as $acl_key => $acl_val) {
|
||||
$acl_post[$acl_val] = 1;
|
||||
}
|
||||
// Users cannot change their own ACL
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)
|
||||
|| ($_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;
|
||||
}
|
||||
// Read all available acl options by calling acl(get)
|
||||
// Set all available acl options we cannot find in the post data to 0, else 1
|
||||
$is_now = acl('get', 'user', $username);
|
||||
if (!empty($is_now)) {
|
||||
foreach ($is_now as $acl_now_name => $acl_now_val) {
|
||||
$set_acls[$acl_now_name] = (isset($acl_post[$acl_now_name])) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'Cannot determine current ACL'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
foreach ($set_acls as $set_acl_key => $set_acl_val) {
|
||||
$stmt = $pdo->prepare("UPDATE `user_acl` SET " . $set_acl_key . " = " . intval($set_acl_val) . "
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => array('acl_saved', $username)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'domainadmin':
|
||||
if ($_SESSION['mailcow_cc_role'] != 'admin') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!is_array($_data['username'])) {
|
||||
$usernames = array();
|
||||
$usernames[] = $_data['username'];
|
||||
}
|
||||
else {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
// Cast to array for single selections
|
||||
$acls = (array)$_data['da_acl'];
|
||||
// Create associative array from index array
|
||||
// All set items are given 1 as value
|
||||
foreach ($acls as $acl_key => $acl_val) {
|
||||
$acl_post[$acl_val] = 1;
|
||||
}
|
||||
// Users cannot change their own ACL
|
||||
if ($_SESSION['mailcow_cc_role'] != 'admin') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Read all available acl options by calling acl(get)
|
||||
// Set all available acl options we cannot find in the post data to 0, else 1
|
||||
$is_now = acl('get', 'domainadmin', $username);
|
||||
if (!empty($is_now)) {
|
||||
foreach ($is_now as $acl_now_name => $acl_now_val) {
|
||||
$set_acls[$acl_now_name] = (isset($acl_post[$acl_now_name])) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'Cannot determine current ACL'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
foreach ($set_acls as $set_acl_key => $set_acl_val) {
|
||||
$stmt = $pdo->prepare("UPDATE `da_acl` SET " . $set_acl_key . " = " . intval($set_acl_val) . "
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => array('acl_saved', $username)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
switch ($_scope) {
|
||||
case 'user':
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT * FROM `user_acl` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $_data));
|
||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
// Domain admins cannot see, add or remove user ACLs they don't have access to by themselves
|
||||
// Editing a user will use acl("get", "user") to determine granted ACLs and therefore block unallowed access escalation via form editing
|
||||
$self_da_acl = acl('get', 'domainadmin', $_SESSION['mailcow_cc_username']);
|
||||
foreach ($self_da_acl as $self_da_acl_key => $self_da_acl_val) {
|
||||
if ($self_da_acl_val == 0) {
|
||||
unset($data[$self_da_acl_key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($data)) {
|
||||
unset($data['username']);
|
||||
return $data;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'domainadmin':
|
||||
if ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin') {
|
||||
return false;
|
||||
}
|
||||
if ($_SESSION['mailcow_cc_role'] == 'domainadmin' && $_SESSION['mailcow_cc_username'] != $_data) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT * FROM `da_acl` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $_data));
|
||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!empty($data)) {
|
||||
unset($data['username']);
|
||||
return $data;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'to_session':
|
||||
if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||
return false;
|
||||
}
|
||||
unset($_SESSION['acl']);
|
||||
$username = strtolower(trim($_SESSION['mailcow_cc_username']));
|
||||
// Admins get access to all modules
|
||||
if ($_SESSION['mailcow_cc_role'] == 'admin' ||
|
||||
(isset($_SESSION["dual-login"]["role"]) && $_SESSION["dual-login"]["role"] == 'admin')) {
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `user_acl` WHERE `Field` != 'username';");
|
||||
$acl_all = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($row = array_shift($acl_all)) {
|
||||
$acl['acl'][$row['Field']] = 1;
|
||||
}
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `da_acl` WHERE `Field` != 'username';");
|
||||
$acl_all = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($row = array_shift($acl_all)) {
|
||||
$acl['acl'][$row['Field']] = 1;
|
||||
}
|
||||
}
|
||||
elseif ($_SESSION['mailcow_cc_role'] == 'domainadmin' ||
|
||||
(isset($_SESSION["dual-login"]["role"]) && $_SESSION["dual-login"]["role"] == 'domainadmin')) {
|
||||
// Read all exting user_acl modules and set to 1
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `user_acl` WHERE `Field` != 'username';");
|
||||
$acl_all = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($row = array_shift($acl_all)) {
|
||||
$acl['acl'][$row['Field']] = 1;
|
||||
}
|
||||
// Read da_acl rules for current user, OVERWRITE overlapping modules
|
||||
// This prevents access to a users sync jobs, when a domain admins was not given access to sync jobs
|
||||
$stmt = $pdo->prepare("SELECT * FROM `da_acl` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => (isset($_SESSION["dual-login"]["username"])) ? $_SESSION["dual-login"]["username"] : $username));
|
||||
$acl_user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
foreach ($acl_user as $acl_user_key => $acl_user_val) {
|
||||
$acl['acl'][$acl_user_key] = $acl_user_val;
|
||||
}
|
||||
unset($acl['acl']['username']);
|
||||
}
|
||||
elseif ($_SESSION['mailcow_cc_role'] == 'user') {
|
||||
$stmt = $pdo->prepare("SELECT * FROM `user_acl` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$acl['acl'] = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
unset($acl['acl']['username']);
|
||||
}
|
||||
if (!empty($acl)) {
|
||||
$_SESSION = array_merge($_SESSION, $acl);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
438
data/web/inc/functions.address_rewriting.inc.php
Executable file
438
data/web/inc/functions.address_rewriting.inc.php
Executable file
|
|
@ -0,0 +1,438 @@
|
|||
<?php
|
||||
function bcc($_action, $_data = null, $_attr = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
return false;
|
||||
}
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
if (!isset($_SESSION['acl']['bcc_maps']) || $_SESSION['acl']['bcc_maps'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$local_dest = strtolower(trim($_data['local_dest']));
|
||||
$bcc_dest = $_data['bcc_dest'];
|
||||
$active = intval($_data['active']);
|
||||
$type = $_data['type'];
|
||||
if ($type != 'sender' && $type != 'rcpt') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'invalid_bcc_map_type'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (empty($bcc_dest)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'bcc_empty'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (is_valid_domain_name($local_dest)) {
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $local_dest)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$domain = idn_to_ascii($local_dest, 0, INTL_IDNA_VARIANT_UTS46);
|
||||
$local_dest_sane = '@' . idn_to_ascii($local_dest, 0, INTL_IDNA_VARIANT_UTS46);
|
||||
}
|
||||
elseif (filter_var($local_dest, FILTER_VALIDATE_EMAIL)) {
|
||||
$mailbox = mailbox('get', 'mailbox_details', $local_dest);
|
||||
$shared_aliases = mailbox('get', 'shared_aliases');
|
||||
$direct_aliases = mailbox('get', 'direct_aliases');
|
||||
if ($mailbox === false && in_array($local_dest, array_merge($direct_aliases, $shared_aliases)) === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $local_dest) &&
|
||||
!hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $local_dest)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$domain = idn_to_ascii(substr(strstr($local_dest, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
$local_dest_sane = $local_dest;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
if (!filter_var($bcc_dest, FILTER_VALIDATE_EMAIL)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('bcc_must_be_email', htmlspecialchars($bcc_dest))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `id` FROM `bcc_maps`
|
||||
WHERE `local_dest` = :local_dest AND `type` = :type");
|
||||
$stmt->execute(array(':local_dest' => $local_dest_sane, ':type' => $type));
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
if ($num_results != 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('bcc_exists', htmlspecialchars($local_dest_sane), $type)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `bcc_maps` (`local_dest`, `bcc_dest`, `domain`, `active`, `type`) VALUES
|
||||
(:local_dest, :bcc_dest, :domain, :active, :type)");
|
||||
$stmt->execute(array(
|
||||
':local_dest' => $local_dest_sane,
|
||||
':bcc_dest' => $bcc_dest,
|
||||
':domain' => $domain,
|
||||
':active' => $active,
|
||||
':type' => $type
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'bcc_saved'
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
if (!isset($_SESSION['acl']['bcc_maps']) || $_SESSION['acl']['bcc_maps'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$is_now = bcc('details', $id);
|
||||
if (!empty($is_now)) {
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$bcc_dest = (!empty($_data['bcc_dest'])) ? $_data['bcc_dest'] : $is_now['bcc_dest'];
|
||||
$local_dest = $is_now['local_dest'];
|
||||
$type = (!empty($_data['type'])) ? $_data['type'] : $is_now['type'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!filter_var($bcc_dest, FILTER_VALIDATE_EMAIL)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('bcc_must_be_email', $bcc_dest)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (empty($bcc_dest)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('bcc_must_be_email', $bcc_dest)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `id` FROM `bcc_maps`
|
||||
WHERE `local_dest` = :local_dest AND `type` = :type");
|
||||
$stmt->execute(array(':local_dest' => $local_dest, ':type' => $type));
|
||||
$id_now = $stmt->fetch(PDO::FETCH_ASSOC)['id'];
|
||||
|
||||
if (isset($id_now) && $id_now != $id) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('bcc_exists', htmlspecialchars($local_dest), $type)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE `bcc_maps` SET `bcc_dest` = :bcc_dest, `active` = :active, `type` = :type WHERE `id`= :id");
|
||||
$stmt->execute(array(
|
||||
':bcc_dest' => $bcc_dest,
|
||||
':active' => $active,
|
||||
':type' => $type,
|
||||
':id' => $id
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('bcc_edited', $bcc_dest)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'details':
|
||||
$bccdata = array();
|
||||
$id = intval($_data);
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `id`,
|
||||
`local_dest`,
|
||||
`bcc_dest`,
|
||||
`active`,
|
||||
`type`,
|
||||
`created`,
|
||||
`domain`,
|
||||
`modified` FROM `bcc_maps`
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$bccdata = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $bccdata['domain'])) {
|
||||
$bccdata = null;
|
||||
return false;
|
||||
}
|
||||
return $bccdata;
|
||||
break;
|
||||
case 'get':
|
||||
$bccdata = array();
|
||||
$all_items = array();
|
||||
$id = intval($_data);
|
||||
|
||||
$stmt = $pdo->query("SELECT `id`, `domain` FROM `bcc_maps`");
|
||||
$all_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($all_items as $i) {
|
||||
if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $i['domain'])) {
|
||||
$bccdata[] = $i['id'];
|
||||
}
|
||||
}
|
||||
$all_items = null;
|
||||
return $bccdata;
|
||||
break;
|
||||
case 'delete':
|
||||
if (!isset($_SESSION['acl']['bcc_maps']) || $_SESSION['acl']['bcc_maps'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
if (!is_numeric($id)) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `domain` FROM `bcc_maps` WHERE id = :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$domain = $stmt->fetch(PDO::FETCH_ASSOC)['domain'];
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `id`= :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('bcc_deleted', $id)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function recipient_map($_action, $_data = null, $attr = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
return false;
|
||||
}
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
$old_dest = strtolower(trim($_data['recipient_map_old']));
|
||||
if (substr($old_dest, 0, 1) == '@') {
|
||||
$old_dest = substr($old_dest, 1);
|
||||
}
|
||||
$new_dest = strtolower(trim($_data['recipient_map_new']));
|
||||
$active = intval($_data['active']);
|
||||
if (is_valid_domain_name($old_dest)) {
|
||||
$old_dest_sane = '@' . idn_to_ascii($old_dest, 0, INTL_IDNA_VARIANT_UTS46);
|
||||
}
|
||||
elseif (filter_var($old_dest, FILTER_VALIDATE_EMAIL)) {
|
||||
$old_dest_sane = $old_dest;
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('invalid_recipient_map_old', htmlspecialchars($old_dest))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!filter_var($new_dest, FILTER_VALIDATE_EMAIL)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('invalid_recipient_map_new', htmlspecialchars($new_dest))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$rmaps = recipient_map('get');
|
||||
foreach ($rmaps as $rmap) {
|
||||
if (recipient_map('details', $rmap)['recipient_map_old'] == $old_dest_sane) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('recipient_map_entry_exists', htmlspecialchars($old_dest_sane))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `recipient_maps` (`old_dest`, `new_dest`, `active`) VALUES
|
||||
(:old_dest, :new_dest, :active)");
|
||||
$stmt->execute(array(
|
||||
':old_dest' => $old_dest_sane,
|
||||
':new_dest' => $new_dest,
|
||||
':active' => $active
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('recipient_map_entry_saved', htmlspecialchars($old_dest_sane))
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$is_now = recipient_map('details', $id);
|
||||
if (!empty($is_now)) {
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$new_dest = (!empty($_data['recipient_map_new'])) ? $_data['recipient_map_new'] : $is_now['recipient_map_new'];
|
||||
$old_dest = (!empty($_data['recipient_map_old'])) ? $_data['recipient_map_old'] : $is_now['recipient_map_old'];
|
||||
if (substr($old_dest, 0, 1) == '@') {
|
||||
$old_dest = substr($old_dest, 1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (is_valid_domain_name($old_dest)) {
|
||||
$old_dest_sane = '@' . idn_to_ascii($old_dest, 0, INTL_IDNA_VARIANT_UTS46);
|
||||
}
|
||||
elseif (filter_var($old_dest, FILTER_VALIDATE_EMAIL)) {
|
||||
$old_dest_sane = $old_dest;
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('invalid_recipient_map_old', htmlspecialchars($old_dest))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!filter_var($new_dest, FILTER_VALIDATE_EMAIL)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('invalid_recipient_map_new', htmlspecialchars($new_dest))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$rmaps = recipient_map('get');
|
||||
foreach ($rmaps as $rmap) {
|
||||
if ($rmap == $id) { continue; }
|
||||
if (recipient_map('details', $rmap)['recipient_map_old'] == $old_dest_sane) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('recipient_map_entry_exists', htmlspecialchars($old_dest_sane))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare("UPDATE `recipient_maps` SET
|
||||
`old_dest` = :old_dest,
|
||||
`new_dest` = :new_dest,
|
||||
`active` = :active
|
||||
WHERE `id`= :id");
|
||||
$stmt->execute(array(
|
||||
':old_dest' => $old_dest_sane,
|
||||
':new_dest' => $new_dest,
|
||||
':active' => $active,
|
||||
':id' => $id
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('recipient_map_entry_saved', htmlspecialchars($old_dest_sane))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'details':
|
||||
$mapdata = array();
|
||||
$id = intval($_data);
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `id`,
|
||||
`old_dest` AS `recipient_map_old`,
|
||||
`new_dest` AS `recipient_map_new`,
|
||||
`active`,
|
||||
`created`,
|
||||
`modified` FROM `recipient_maps`
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$mapdata = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $mapdata;
|
||||
break;
|
||||
case 'get':
|
||||
$mapdata = array();
|
||||
$all_items = array();
|
||||
$id = intval($_data);
|
||||
|
||||
$stmt = $pdo->query("SELECT `id` FROM `recipient_maps`");
|
||||
$all_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($all_items as $i) {
|
||||
$mapdata[] = $i['id'];
|
||||
}
|
||||
$all_items = null;
|
||||
return $mapdata;
|
||||
break;
|
||||
case 'delete':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
if (!is_numeric($id)) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `recipient_maps` WHERE `id`= :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('recipient_map_entry_deleted', htmlspecialchars($id))
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
246
data/web/inc/functions.admin.inc.php
Executable file
246
data/web/inc/functions.admin.inc.php
Executable file
|
|
@ -0,0 +1,246 @@
|
|||
<?php
|
||||
function admin($_action, $_data = null) {
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
|
||||
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
$username = strtolower(trim($_data['username']));
|
||||
$password = $_data['password'];
|
||||
$password2 = $_data['password2'];
|
||||
$active = intval($_data['active']);
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `admin`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
foreach ($num_results as $num_results_each) {
|
||||
if ($num_results_each != 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_exists', htmlspecialchars($username))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
|
||||
VALUES (:username, :password_hashed, '1', :active)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':password_hashed' => $password_hashed,
|
||||
':active' => $active
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('admin_added', htmlspecialchars($username))
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
if (!is_array($_data['username'])) {
|
||||
$usernames = array();
|
||||
$usernames[] = $_data['username'];
|
||||
}
|
||||
else {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
$is_now = admin('details', $username);
|
||||
if (!empty($is_now)) {
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$password = $_data['password'];
|
||||
$password2 = $_data['password2'];
|
||||
if ($active == 0) {
|
||||
$left_active = 0;
|
||||
foreach (admin('get') as $admin) {
|
||||
$left_active = $left_active + admin('details', $admin)['active'];
|
||||
}
|
||||
if ($left_active == 1) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'no_active_admin'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username_new)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ($username_new != $username) {
|
||||
if (!empty(admin('details', $username_new)['username'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username_new)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||
}
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('admin_modified', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
case 'delete':
|
||||
$usernames = (array)$_data['username'];
|
||||
foreach ($usernames as $username) {
|
||||
if ($_SESSION['mailcow_cc_username'] == $username) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'cannot_delete_self'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (empty(admin('details', $username))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('admin_removed', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
$admins = array();
|
||||
$stmt = $pdo->query("SELECT `username` FROM `admin` WHERE `superadmin` = '1'");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($row = array_shift($rows)) {
|
||||
$admins[] = $row['username'];
|
||||
}
|
||||
return $admins;
|
||||
break;
|
||||
case 'details':
|
||||
$admindata = array();
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
`tfa`.`active` AS `tfa_active`,
|
||||
`admin`.`username`,
|
||||
`admin`.`created`,
|
||||
`admin`.`active` AS `active`
|
||||
FROM `admin`
|
||||
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`admin`.`username`
|
||||
WHERE `admin`.`username`= :admin AND `superadmin` = '1'");
|
||||
$stmt->execute(array(
|
||||
':admin' => $_data
|
||||
));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row)) {
|
||||
return false;
|
||||
}
|
||||
$admindata['username'] = $row['username'];
|
||||
$admindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
||||
$admindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
||||
$admindata['active'] = $row['active'];
|
||||
$admindata['active_int'] = $row['active'];
|
||||
$admindata['created'] = $row['created'];
|
||||
return $admindata;
|
||||
break;
|
||||
}
|
||||
}
|
||||
242
data/web/inc/functions.app_passwd.inc.php
Executable file
242
data/web/inc/functions.app_passwd.inc.php
Executable file
|
|
@ -0,0 +1,242 @@
|
|||
<?php
|
||||
function app_passwd($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['app_passwd']) ?: $_data_log['app_passwd'] = '*';
|
||||
!isset($_data_log['app_passwd2']) ?: $_data_log['app_passwd2'] = '*';
|
||||
if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) {
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
$username = $_data['username'];
|
||||
}
|
||||
}
|
||||
else {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
}
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
$app_name = htmlspecialchars(trim($_data['app_name']));
|
||||
$password = $_data['app_passwd'];
|
||||
$password2 = $_data['app_passwd2'];
|
||||
$active = intval($_data['active']);
|
||||
$protocols = (array)$_data['protocols'];
|
||||
$imap_access = (in_array('imap_access', $protocols)) ? 1 : 0;
|
||||
$dav_access = (in_array('dav_access', $protocols)) ? 1 : 0;
|
||||
$smtp_access = (in_array('smtp_access', $protocols)) ? 1 : 0;
|
||||
$eas_access = (in_array('eas_access', $protocols)) ? 1 : 0;
|
||||
$pop3_access = (in_array('pop3_access', $protocols)) ? 1 : 0;
|
||||
$sieve_access = (in_array('sieve_access', $protocols)) ? 1 : 0;
|
||||
$domain = mailbox('get', 'mailbox_details', $username)['domain'];
|
||||
if (empty($domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'password_complexity'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($password != $password2) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'password_mismatch'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
if (empty($app_name)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'app_name_empty'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `app_passwd` (`name`, `mailbox`, `domain`, `password`, `imap_access`, `smtp_access`, `eas_access`, `dav_access`, `pop3_access`, `sieve_access`, `active`)
|
||||
VALUES (:app_name, :mailbox, :domain, :password, :imap_access, :smtp_access, :eas_access, :dav_access, :pop3_access, :sieve_access, :active)");
|
||||
$stmt->execute(array(
|
||||
':app_name' => $app_name,
|
||||
':mailbox' => $username,
|
||||
':domain' => $domain,
|
||||
':password' => $password_hashed,
|
||||
':imap_access' => $imap_access,
|
||||
':smtp_access' => $smtp_access,
|
||||
':eas_access' => $eas_access,
|
||||
':dav_access' => $dav_access,
|
||||
':pop3_access' => $pop3_access,
|
||||
':sieve_access' => $sieve_access,
|
||||
':active' => $active
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'app_passwd_added'
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$is_now = app_passwd('details', $id);
|
||||
if (!empty($is_now)) {
|
||||
$app_name = (!empty($_data['app_name'])) ? $_data['app_name'] : $is_now['name'];
|
||||
$password = (!empty($_data['password'])) ? $_data['password'] : null;
|
||||
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
|
||||
if (isset($_data['protocols'])) {
|
||||
$protocols = (array)$_data['protocols'];
|
||||
$imap_access = (in_array('imap_access', $protocols)) ? 1 : 0;
|
||||
$dav_access = (in_array('dav_access', $protocols)) ? 1 : 0;
|
||||
$smtp_access = (in_array('smtp_access', $protocols)) ? 1 : 0;
|
||||
$eas_access = (in_array('eas_access', $protocols)) ? 1 : 0;
|
||||
$pop3_access = (in_array('pop3_access', $protocols)) ? 1 : 0;
|
||||
$sieve_access = (in_array('sieve_access', $protocols)) ? 1 : 0;
|
||||
}
|
||||
else {
|
||||
$imap_access = $is_now['imap_access'];
|
||||
$smtp_access = $is_now['smtp_access'];
|
||||
$dav_access = $is_now['dav_access'];
|
||||
$eas_access = $is_now['eas_access'];
|
||||
$pop3_access = $is_now['pop3_access'];
|
||||
$sieve_access = $is_now['sieve_access'];
|
||||
}
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('app_passwd_id_invalid', $id)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$app_name = htmlspecialchars(trim($app_name));
|
||||
if (!empty($password) && !empty($password2)) {
|
||||
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'password_complexity'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ($password != $password2) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'password_mismatch'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `app_passwd` SET
|
||||
`password` = :password_hashed
|
||||
WHERE `mailbox` = :username AND `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username' => $username,
|
||||
':id' => $id
|
||||
));
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE `app_passwd` SET
|
||||
`name` = :app_name,
|
||||
`mailbox` = :username,
|
||||
`imap_access` = :imap_access,
|
||||
`smtp_access` = :smtp_access,
|
||||
`eas_access` = :eas_access,
|
||||
`dav_access` = :dav_access,
|
||||
`pop3_access` = :pop3_access,
|
||||
`sieve_access` = :sieve_access,
|
||||
`active` = :active
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':app_name' => $app_name,
|
||||
':username' => $username,
|
||||
':imap_access' => $imap_access,
|
||||
':smtp_access' => $smtp_access,
|
||||
':eas_access' => $eas_access,
|
||||
':dav_access' => $dav_access,
|
||||
':pop3_access' => $pop3_access,
|
||||
':sieve_access' => $sieve_access,
|
||||
':active' => $active,
|
||||
':id' => $id
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_modified', htmlspecialchars(implode(', ', $ids)))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$stmt = $pdo->prepare("SELECT `mailbox` FROM `app_passwd` WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$mailbox = $stmt->fetch(PDO::FETCH_ASSOC)['mailbox'];
|
||||
if (empty($mailbox)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'app_passwd_id_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $mailbox)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `app_passwd` WHERE `id`= :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('app_passwd_removed', htmlspecialchars($id))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
$app_passwds = array();
|
||||
$stmt = $pdo->prepare("SELECT `id`, `name` FROM `app_passwd` WHERE `mailbox` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$app_passwds = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $app_passwds;
|
||||
break;
|
||||
case 'details':
|
||||
$app_passwd_data = array();
|
||||
$stmt = $pdo->prepare("SELECT *
|
||||
FROM `app_passwd`
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$app_passwd_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($app_passwd_data)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $app_passwd_data['mailbox'])) {
|
||||
$app_passwd_data = array();
|
||||
return false;
|
||||
}
|
||||
$app_passwd_data['name'] = htmlspecialchars(trim($app_passwd_data['name']));
|
||||
return $app_passwd_data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
341
data/web/inc/functions.customize.inc.php
Executable file
341
data/web/inc/functions.customize.inc.php
Executable file
|
|
@ -0,0 +1,341 @@
|
|||
<?php
|
||||
function customize($_action, $_item, $_data = null) {
|
||||
global $redis;
|
||||
global $lang;
|
||||
global $LOGO_LIMITS;
|
||||
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
// disable functionality when demo mode is enabled
|
||||
if ($GLOBALS["DEMO_MODE"]) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'demo_mode_enabled'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
switch ($_item) {
|
||||
case 'main_logo':
|
||||
case 'main_logo_dark':
|
||||
if (in_array($_data[$_item]['type'], array('image/gif', 'image/jpeg', 'image/pjpeg', 'image/x-png', 'image/png', 'image/svg+xml'))) {
|
||||
try {
|
||||
if (file_exists($_data[$_item]['tmp_name']) !== true) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'img_tmp_missing'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($_data[$_item]['size'] > $LOGO_LIMITS['max_size']) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'img_size_exceeded'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
list($width, $height) = getimagesize($_data[$_item]['tmp_name']);
|
||||
if ($width > $LOGO_LIMITS['max_width'] || $height > $LOGO_LIMITS['max_height']) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'img_dimensions_exceeded'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$image = new Imagick($_data[$_item]['tmp_name']);
|
||||
if ($image->valid() !== true) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'img_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$image->destroy();
|
||||
}
|
||||
catch (ImagickException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'img_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'invalid_mime_type'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$redis->Set(strtoupper($_item), 'data:' . $_data[$_item]['type'] . ';base64,' . base64_encode(file_get_contents($_data[$_item]['tmp_name'])));
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'upload_success'
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'edit':
|
||||
// disable functionality when demo mode is enabled
|
||||
if ($GLOBALS["DEMO_MODE"]) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'demo_mode_enabled'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
switch ($_item) {
|
||||
case 'app_links':
|
||||
$apps = (array)$_data['app'];
|
||||
$links = (array)$_data['href'];
|
||||
$out = array();
|
||||
if (count($apps) == count($links)) {
|
||||
for ($i = 0; $i < count($apps); $i++) {
|
||||
$out[] = array($apps[$i] => $links[$i]);
|
||||
}
|
||||
try {
|
||||
$redis->set('APP_LINKS', json_encode($out));
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'app_links'
|
||||
);
|
||||
break;
|
||||
case 'ui_texts':
|
||||
$title_name = $_data['title_name'];
|
||||
$main_name = $_data['main_name'];
|
||||
$apps_name = $_data['apps_name'];
|
||||
$help_text = $_data['help_text'];
|
||||
$ui_footer = $_data['ui_footer'];
|
||||
$ui_announcement_text = $_data['ui_announcement_text'];
|
||||
$ui_announcement_type = (in_array($_data['ui_announcement_type'], array('info', 'warning', 'danger'))) ? $_data['ui_announcement_type'] : false;
|
||||
$ui_announcement_active = (!empty($_data['ui_announcement_active']) ? 1 : 0);
|
||||
|
||||
try {
|
||||
$redis->set('TITLE_NAME', htmlspecialchars($title_name));
|
||||
$redis->set('MAIN_NAME', htmlspecialchars($main_name));
|
||||
$redis->set('APPS_NAME', htmlspecialchars($apps_name));
|
||||
$redis->set('HELP_TEXT', $help_text);
|
||||
$redis->set('UI_FOOTER', $ui_footer);
|
||||
$redis->set('UI_ANNOUNCEMENT_TEXT', $ui_announcement_text);
|
||||
$redis->set('UI_ANNOUNCEMENT_TYPE', $ui_announcement_type);
|
||||
$redis->set('UI_ANNOUNCEMENT_ACTIVE', $ui_announcement_active);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'ui_texts'
|
||||
);
|
||||
break;
|
||||
case 'ip_check':
|
||||
$ip_check = ($_data['ip_check_opt_in'] == "1") ? 1 : 0;
|
||||
try {
|
||||
$redis->set('IP_CHECK', $ip_check);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'ip_check_opt_in_modified'
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
// disable functionality when demo mode is enabled
|
||||
if ($GLOBALS["DEMO_MODE"]) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'demo_mode_enabled'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
switch ($_item) {
|
||||
case 'main_logo':
|
||||
case 'main_logo_dark':
|
||||
try {
|
||||
if ($redis->del(strtoupper($_item))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'reset_main_logo'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
switch ($_item) {
|
||||
case 'app_links':
|
||||
try {
|
||||
$app_links = json_decode($redis->get('APP_LINKS'), true);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return ($app_links) ? $app_links : false;
|
||||
break;
|
||||
case 'main_logo':
|
||||
case 'main_logo_dark':
|
||||
try {
|
||||
return $redis->get(strtoupper($_item));
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'ui_texts':
|
||||
try {
|
||||
$data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : 'mailcow UI';
|
||||
$data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : 'mailcow UI';
|
||||
$data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : $lang['header']['apps'];
|
||||
$data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false;
|
||||
if (!empty($redis->get('UI_IMPRESS'))) {
|
||||
$redis->set('UI_FOOTER', $redis->get('UI_IMPRESS'));
|
||||
$redis->del('UI_IMPRESS');
|
||||
}
|
||||
$data['ui_footer'] = ($ui_footer = $redis->get('UI_FOOTER')) ? $ui_footer : false;
|
||||
$data['ui_announcement_text'] = ($ui_announcement_text = $redis->get('UI_ANNOUNCEMENT_TEXT')) ? $ui_announcement_text : false;
|
||||
$data['ui_announcement_type'] = ($ui_announcement_type = $redis->get('UI_ANNOUNCEMENT_TYPE')) ? $ui_announcement_type : false;
|
||||
$data['ui_announcement_active'] = ($redis->get('UI_ANNOUNCEMENT_ACTIVE') == 1) ? 1 : 0;
|
||||
return $data;
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'main_logo_specs':
|
||||
case 'main_logo_dark_specs':
|
||||
try {
|
||||
$image = new Imagick();
|
||||
if($_item == 'main_logo_specs') {
|
||||
$img_data = explode('base64,', customize('get', 'main_logo'));
|
||||
} else {
|
||||
$img_data = explode('base64,', customize('get', 'main_logo_dark'));
|
||||
}
|
||||
if ($img_data[1]) {
|
||||
$image->readImageBlob(base64_decode($img_data[1]));
|
||||
return $image->identifyImage();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (ImagickException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'imagick_exception'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'ip_check':
|
||||
try {
|
||||
$ip_check = ($ip_check = $redis->get('IP_CHECK')) ? $ip_check : 0;
|
||||
return $ip_check;
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
325
data/web/inc/functions.dkim.inc.php
Executable file
325
data/web/inc/functions.dkim.inc.php
Executable file
|
|
@ -0,0 +1,325 @@
|
|||
<?php
|
||||
|
||||
function dkim($_action, $_data = null, $privkey = false) {
|
||||
global $redis;
|
||||
global $lang;
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
$key_length = intval($_data['key_size']);
|
||||
$dkim_selector = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : 'dkim';
|
||||
$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' => array('dkim_domain_or_sel_invalid', $domain)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ($redis->hGet('DKIM_PUB_KEYS', $domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('dkim_domain_or_sel_invalid', $domain)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!ctype_alnum(str_replace(['-', '_'], '', $dkim_selector))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('dkim_domain_or_sel_invalid', $domain)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('access_denied', $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_PUB_KEYS', $domain, $pubKey);
|
||||
$redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
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, true);
|
||||
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, base64_decode(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' => array('dkim_duplicated', $from_domain, $to_domain)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'import':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$private_key_input = trim($_data['private_key_file']);
|
||||
$overwrite_existing = intval($_data['overwrite_existing']);
|
||||
$private_key_normalized = preg_replace('~\r\n?~', "\n", $private_key_input);
|
||||
$private_key = openssl_pkey_get_private($private_key_normalized);
|
||||
if ($ssl_error = openssl_error_string()) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('private_key_error', $ssl_error)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Explode by nl
|
||||
$pem_public_key_array = explode(PHP_EOL, trim(openssl_pkey_get_details($private_key)['key']));
|
||||
// Remove first and last line/item
|
||||
array_shift($pem_public_key_array);
|
||||
array_pop($pem_public_key_array);
|
||||
// Implode as single string
|
||||
$pem_public_key = implode('', (array)$pem_public_key_array);
|
||||
$dkim_selector = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : 'dkim';
|
||||
$domain = $_data['domain'];
|
||||
if (!is_valid_domain_name($domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('dkim_domain_or_sel_invalid', $domain)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($redis->hGet('DKIM_PUB_KEYS', $domain)) {
|
||||
if ($overwrite_existing == 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('dkim_domain_or_sel_exists', $domain)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!ctype_alnum($dkim_selector)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('dkim_domain_or_sel_invalid', $domain)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
dkim('delete', array('domains' => $domain));
|
||||
$redis->hSet('DKIM_PUB_KEYS', $domain, $pem_public_key);
|
||||
$redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
|
||||
$redis->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, $private_key_normalized);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
unset($private_key_normalized);
|
||||
unset($private_key);
|
||||
unset($private_key_input);
|
||||
try {
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('dkim_added', $domain)
|
||||
);
|
||||
return true;
|
||||
break;
|
||||
case 'details':
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data) && $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
return false;
|
||||
}
|
||||
$dkimdata = array();
|
||||
if ($redis_dkim_key_data = $redis->hGet('DKIM_PUB_KEYS', $_data)) {
|
||||
$dkimdata['pubkey'] = $redis_dkim_key_data;
|
||||
if (strlen($dkimdata['pubkey']) < 391) {
|
||||
$dkimdata['length'] = "1024";
|
||||
}
|
||||
elseif (strlen($dkimdata['pubkey']) < 736) {
|
||||
$dkimdata['length'] = "2048";
|
||||
}
|
||||
elseif (strlen($dkimdata['pubkey']) < 1416) {
|
||||
$dkimdata['length'] = "4096";
|
||||
}
|
||||
else {
|
||||
$dkimdata['length'] = ">= 8192";
|
||||
}
|
||||
if ($GLOBALS['SPLIT_DKIM_255'] === true) {
|
||||
$dkim_txt_tmp = str_split('v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data, 255);
|
||||
$dkimdata['dkim_txt'] = sprintf('"%s"', implode('" "', (array)$dkim_txt_tmp ) );
|
||||
}
|
||||
else {
|
||||
$dkimdata['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data;
|
||||
}
|
||||
$dkimdata['dkim_selector'] = $redis->hGet('DKIM_SELECTORS', $_data);
|
||||
if ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] || $privkey == true) {
|
||||
$dkimdata['privkey'] = base64_encode($redis->hGet('DKIM_PRIV_KEYS', $dkimdata['dkim_selector'] . '.' . $_data));
|
||||
}
|
||||
else {
|
||||
$dkimdata['privkey'] = '';
|
||||
}
|
||||
}
|
||||
return $dkimdata;
|
||||
break;
|
||||
case 'blind':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$blinddkim = array();
|
||||
foreach ($redis->hKeys('DKIM_PUB_KEYS') as $redis_dkim_domain) {
|
||||
$blinddkim[] = $redis_dkim_domain;
|
||||
}
|
||||
return array_diff($blinddkim, array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')));
|
||||
break;
|
||||
case 'delete':
|
||||
$domains = (array)$_data['domains'];
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
foreach ($domains as $domain) {
|
||||
if (!is_valid_domain_name($domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('dkim_domain_or_sel_invalid', $domain)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$selector = $redis->hGet('DKIM_SELECTORS', $domain);
|
||||
$redis->hDel('DKIM_PUB_KEYS', $domain);
|
||||
$redis->hDel('DKIM_PRIV_KEYS', $selector . '.' . $domain);
|
||||
$redis->hDel('DKIM_SELECTORS', $domain);
|
||||
}
|
||||
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_removed', htmlspecialchars($domain))
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
207
data/web/inc/functions.docker.inc.php
Executable file
207
data/web/inc/functions.docker.inc.php
Executable file
|
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $extra_headers = null) {
|
||||
global $DOCKER_TIMEOUT;
|
||||
global $redis;
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: application/json' ));
|
||||
// We are using our mail certificates for dockerapi, the names will not match, the certs are trusted anyway
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
switch($action) {
|
||||
case 'get_id':
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 0);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
|
||||
$response = curl_exec($curl);
|
||||
if ($response === false) {
|
||||
$err = curl_error($curl);
|
||||
curl_close($curl);
|
||||
return $err;
|
||||
}
|
||||
else {
|
||||
curl_close($curl);
|
||||
$containers = json_decode($response, true);
|
||||
if (!empty($containers)) {
|
||||
foreach ($containers as $container) {
|
||||
if (isset($container['Config']['Labels']['com.docker.compose.service'])
|
||||
&& $container['Config']['Labels']['com.docker.compose.service'] == $service_name
|
||||
&& strtolower($container['Config']['Labels']['com.docker.compose.project']) == strtolower(getenv('COMPOSE_PROJECT_NAME'))) {
|
||||
return trim($container['Id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
case 'containers':
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 0);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
|
||||
$response = curl_exec($curl);
|
||||
if ($response === false) {
|
||||
$err = curl_error($curl);
|
||||
curl_close($curl);
|
||||
return $err;
|
||||
}
|
||||
else {
|
||||
curl_close($curl);
|
||||
$containers = json_decode($response, true);
|
||||
if (!empty($containers)) {
|
||||
foreach ($containers as $container) {
|
||||
if (strtolower($container['Config']['Labels']['com.docker.compose.project']) == strtolower(getenv('COMPOSE_PROJECT_NAME'))) {
|
||||
$out[$container['Config']['Labels']['com.docker.compose.service']]['State'] = $container['State'];
|
||||
$out[$container['Config']['Labels']['com.docker.compose.service']]['Config'] = $container['Config'];
|
||||
$out[$container['Config']['Labels']['com.docker.compose.service']]['Id'] = trim($container['Id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (!empty($out)) ? $out : false;
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
case 'info':
|
||||
if (empty($service_name)) {
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 0);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
|
||||
}
|
||||
else {
|
||||
$container_id = docker('get_id', $service_name);
|
||||
if (ctype_xdigit($container_id)) {
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/' . $container_id . '/json');
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 0);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
|
||||
$response = curl_exec($curl);
|
||||
if ($response === false) {
|
||||
$err = curl_error($curl);
|
||||
curl_close($curl);
|
||||
return $err;
|
||||
}
|
||||
else {
|
||||
curl_close($curl);
|
||||
$decoded_response = json_decode($response, true);
|
||||
if (!empty($decoded_response)) {
|
||||
if (empty($service_name)) {
|
||||
foreach ($decoded_response as $container) {
|
||||
if (isset($container['Config']['Labels']['com.docker.compose.project'])
|
||||
&& strtolower($container['Config']['Labels']['com.docker.compose.project']) == strtolower(getenv('COMPOSE_PROJECT_NAME'))) {
|
||||
unset($container['Config']['Env']);
|
||||
$out[$container['Config']['Labels']['com.docker.compose.service']]['State'] = $container['State'];
|
||||
$out[$container['Config']['Labels']['com.docker.compose.service']]['Config'] = $container['Config'];
|
||||
$out[$container['Config']['Labels']['com.docker.compose.service']]['Id'] = trim($container['Id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (isset($decoded_response['Config']['Labels']['com.docker.compose.project'])
|
||||
&& strtolower($decoded_response['Config']['Labels']['com.docker.compose.project']) == strtolower(getenv('COMPOSE_PROJECT_NAME'))) {
|
||||
unset($container['Config']['Env']);
|
||||
$out[$decoded_response['Config']['Labels']['com.docker.compose.service']]['State'] = $decoded_response['State'];
|
||||
$out[$decoded_response['Config']['Labels']['com.docker.compose.service']]['Config'] = $decoded_response['Config'];
|
||||
$out[$decoded_response['Config']['Labels']['com.docker.compose.service']]['Id'] = trim($decoded_response['Id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($response)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return (!empty($out)) ? $out : false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'post':
|
||||
if (!empty($attr1)) {
|
||||
$container_id = docker('get_id', $service_name);
|
||||
if (ctype_xdigit($container_id) && ctype_alnum($attr1)) {
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/' . $container_id . '/' . $attr1);
|
||||
curl_setopt($curl, CURLOPT_POST, 1);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
|
||||
if (!empty($attr2)) {
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($attr2));
|
||||
}
|
||||
if (!empty($extra_headers) && is_array($extra_headers)) {
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, $extra_headers);
|
||||
}
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
$response = curl_exec($curl);
|
||||
if ($response === false) {
|
||||
$err = curl_error($curl);
|
||||
curl_close($curl);
|
||||
return $err;
|
||||
}
|
||||
else {
|
||||
curl_close($curl);
|
||||
if (empty($response)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'container_stats':
|
||||
if (empty($service_name)){
|
||||
return false;
|
||||
}
|
||||
|
||||
$container_id = $service_name;
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/container/' . $container_id . '/stats/update');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 1);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
|
||||
$response = curl_exec($curl);
|
||||
if ($response === false) {
|
||||
$err = curl_error($curl);
|
||||
curl_close($curl);
|
||||
return $err;
|
||||
}
|
||||
else {
|
||||
curl_close($curl);
|
||||
$stats = json_decode($response, true);
|
||||
if (!empty($stats)) return $stats;
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
case 'host_stats':
|
||||
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/host/stats');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 0);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
|
||||
$response = curl_exec($curl);
|
||||
if ($response === false) {
|
||||
$err = curl_error($curl);
|
||||
curl_close($curl);
|
||||
return $err;
|
||||
}
|
||||
else {
|
||||
curl_close($curl);
|
||||
$stats = json_decode($response, true);
|
||||
if (!empty($stats)) return $stats;
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
case 'broadcast':
|
||||
$request = array(
|
||||
"api_call" => "container_post",
|
||||
"container_name" => $service_name,
|
||||
"post_action" => $attr1,
|
||||
"request" => $attr2
|
||||
);
|
||||
|
||||
$redis->publish("MC_CHANNEL", json_encode($request));
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
468
data/web/inc/functions.domain_admin.inc.php
Executable file
468
data/web/inc/functions.domain_admin.inc.php
Executable file
|
|
@ -0,0 +1,468 @@
|
|||
<?php
|
||||
function domain_admin($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
|
||||
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
|
||||
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
|
||||
!isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
|
||||
!isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
$username = strtolower(trim($_data['username']));
|
||||
$password = $_data['password'];
|
||||
$password2 = $_data['password2'];
|
||||
$domains = (array)$_data['domains'];
|
||||
$active = intval($_data['active']);
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (empty($domains)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'domain_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `admin`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
foreach ($num_results as $num_results_each) {
|
||||
if ($num_results_each != 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_exists', htmlspecialchars($username))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (password_check($password, $password2) !== true) {
|
||||
continue;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$valid_domains = 0;
|
||||
foreach ($domains as $domain) {
|
||||
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_invalid', htmlspecialchars($domain))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$valid_domains++;
|
||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
||||
VALUES (:username, :domain, :created, :active)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':domain' => $domain,
|
||||
':created' => date('Y-m-d H:i:s'),
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
if ($valid_domains != 0) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
|
||||
VALUES (:username, :password_hashed, '0', :active)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':password_hashed' => $password_hashed,
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_added', htmlspecialchars($username))
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Administrator
|
||||
if ($_SESSION['mailcow_cc_role'] == "admin") {
|
||||
if (!is_array($_data['username'])) {
|
||||
$usernames = array();
|
||||
$usernames[] = $_data['username'];
|
||||
}
|
||||
else {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
$is_now = domain_admin('details', $username);
|
||||
$domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null;
|
||||
if (!empty($is_now)) {
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$domains = (!empty($domains)) ? $domains : $is_now['selected_domains'];
|
||||
$username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$password = $_data['password'];
|
||||
$password2 = $_data['password2'];
|
||||
if (!empty($domains)) {
|
||||
foreach ($domains as $domain) {
|
||||
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_invalid', htmlspecialchars($domain))
|
||||
);
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username_new)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ($username_new != $username) {
|
||||
if (!empty(domain_admin('details', $username_new)['username'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username_new)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username
|
||||
));
|
||||
if (!empty($domains)) {
|
||||
foreach ($domains as $domain) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
||||
VALUES (:username_new, :domain, :created, :active)");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':domain' => $domain,
|
||||
':created' => date('Y-m-d H:i:s'),
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
}
|
||||
if (!empty($password)) {
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username_new' => $username_new,
|
||||
':username' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
if (isset($_data['disable_tfa'])) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||
}
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_modified', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Domain administrator
|
||||
// Can only edit itself
|
||||
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
$password_old = $_data['user_old_pass'];
|
||||
$password_new = $_data['user_new_pass'];
|
||||
$password_new2 = $_data['user_new_pass2'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (password_check($password_new, $password_new2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password_new);
|
||||
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username' => $username
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_modified', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$usernames = (array)$_data['username'];
|
||||
foreach ($usernames as $username) {
|
||||
if (empty(domain_admin('details', $username))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('username_invalid', $username)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('domain_admin_removed', htmlspecialchars($username))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
$domainadmins = array();
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->query("SELECT DISTINCT
|
||||
`username`
|
||||
FROM `domain_admins`
|
||||
WHERE `username` IN (
|
||||
SELECT `username` FROM `admin`
|
||||
WHERE `superadmin`!='1'
|
||||
)");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($row = array_shift($rows)) {
|
||||
$domainadmins[] = $row['username'];
|
||||
}
|
||||
return $domainadmins;
|
||||
break;
|
||||
case 'details':
|
||||
$domainadmindata = array();
|
||||
if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) {
|
||||
return false;
|
||||
}
|
||||
elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
|
||||
return false;
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
`tfa`.`active` AS `tfa_active`,
|
||||
`domain_admins`.`username`,
|
||||
`domain_admins`.`created`,
|
||||
`domain_admins`.`active` AS `active`
|
||||
FROM `domain_admins`
|
||||
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
|
||||
WHERE `domain_admins`.`username`= :domain_admin");
|
||||
$stmt->execute(array(
|
||||
':domain_admin' => $_data
|
||||
));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row)) {
|
||||
return false;
|
||||
}
|
||||
$domainadmindata['username'] = $row['username'];
|
||||
$domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
||||
$domainadmindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
||||
$domainadmindata['active'] = $row['active'];
|
||||
$domainadmindata['active_int'] = $row['active'];
|
||||
$domainadmindata['created'] = $row['created'];
|
||||
// GET SELECTED
|
||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
||||
WHERE `domain` IN (
|
||||
SELECT `domain` FROM `domain_admins`
|
||||
WHERE `username`= :domain_admin)");
|
||||
$stmt->execute(array(':domain_admin' => $_data));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$domainadmindata['selected_domains'][] = $row['domain'];
|
||||
}
|
||||
// GET UNSELECTED
|
||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
||||
WHERE `domain` NOT IN (
|
||||
SELECT `domain` FROM `domain_admins`
|
||||
WHERE `username`= :domain_admin)");
|
||||
$stmt->execute(array(':domain_admin' => $_data));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$domainadmindata['unselected_domains'][] = $row['domain'];
|
||||
}
|
||||
if (!isset($domainadmindata['unselected_domains'])) {
|
||||
$domainadmindata['unselected_domains'] = "";
|
||||
}
|
||||
|
||||
return $domainadmindata;
|
||||
break;
|
||||
}
|
||||
}
|
||||
function domain_admin_sso($_action, $_data) {
|
||||
global $pdo;
|
||||
|
||||
switch ($_action) {
|
||||
case 'check':
|
||||
$token = $_data;
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `t1`.`username` FROM `da_sso` AS `t1` JOIN `admin` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1 AND `t2`.`superadmin` = 0;");
|
||||
$stmt->execute(array(
|
||||
':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token)
|
||||
));
|
||||
$return = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return empty($return['username']) ? false : $return['username'];
|
||||
case 'issue':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$username = $_data['username'];
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
|
||||
if ($num_results < 1) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => array('object_doesnt_exist', htmlspecialchars($username))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = implode('-', array(
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3)))
|
||||
));
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO `da_sso` (`username`, `token`)
|
||||
VALUES (:username, :token)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':token' => $token
|
||||
));
|
||||
|
||||
// perform cleanup
|
||||
$pdo->query("DELETE FROM `da_sso` WHERE created < DATE_SUB(NOW(), INTERVAL '30' SECOND);");
|
||||
|
||||
return ['token' => $token];
|
||||
break;
|
||||
}
|
||||
}
|
||||
402
data/web/inc/functions.fail2ban.inc.php
Executable file
402
data/web/inc/functions.fail2ban.inc.php
Executable file
|
|
@ -0,0 +1,402 @@
|
|||
<?php
|
||||
function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
global $redis;
|
||||
$_data_log = $_data;
|
||||
switch ($_action) {
|
||||
case 'get':
|
||||
$f2b_options = array();
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true);
|
||||
$f2b_options['regex'] = json_decode($redis->Get('F2B_REGEX'), true);
|
||||
$wl = $redis->hGetAll('F2B_WHITELIST');
|
||||
if (is_array($wl)) {
|
||||
foreach ($wl as $key => $value) {
|
||||
$tmp_wl_data[] = $key;
|
||||
}
|
||||
if (isset($tmp_wl_data)) {
|
||||
natsort($tmp_wl_data);
|
||||
$f2b_options['whitelist'] = implode(PHP_EOL, (array)$tmp_wl_data);
|
||||
}
|
||||
else {
|
||||
$f2b_options['whitelist'] = "";
|
||||
}
|
||||
}
|
||||
else {
|
||||
$f2b_options['whitelist'] = "";
|
||||
}
|
||||
$bl = $redis->hGetAll('F2B_BLACKLIST');
|
||||
if (is_array($bl)) {
|
||||
foreach ($bl as $key => $value) {
|
||||
$tmp_bl_data[] = $key;
|
||||
}
|
||||
if (isset($tmp_bl_data)) {
|
||||
natsort($tmp_bl_data);
|
||||
$f2b_options['blacklist'] = implode(PHP_EOL, (array)$tmp_bl_data);
|
||||
}
|
||||
else {
|
||||
$f2b_options['blacklist'] = "";
|
||||
}
|
||||
}
|
||||
else {
|
||||
$f2b_options['blacklist'] = "";
|
||||
}
|
||||
$pb = $redis->hGetAll('F2B_PERM_BANS');
|
||||
if (is_array($pb)) {
|
||||
foreach ($pb as $key => $value) {
|
||||
$f2b_options['perm_bans'][] = array(
|
||||
'network'=>$key,
|
||||
'ip' => strtok($key,'/')
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
$f2b_options['perm_bans'] = "";
|
||||
}
|
||||
$active_bans = $redis->hGetAll('F2B_ACTIVE_BANS');
|
||||
$queue_unban = $redis->hGetAll('F2B_QUEUE_UNBAN');
|
||||
if (is_array($active_bans)) {
|
||||
foreach ($active_bans as $network => $banned_until) {
|
||||
$queued_for_unban = (isset($queue_unban[$network]) && $queue_unban[$network] == 1) ? 1 : 0;
|
||||
$difference = $banned_until - time();
|
||||
$f2b_options['active_bans'][] = array(
|
||||
'queued_for_unban' => $queued_for_unban,
|
||||
'network' => $network,
|
||||
'ip' => strtok($network,'/'),
|
||||
'banned_until' => sprintf('%02dh %02dm %02ds', ($difference/3600), ($difference/60%60), $difference%60)
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$f2b_options['active_bans'] = "";
|
||||
}
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return $f2b_options;
|
||||
break;
|
||||
case 'edit':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Start to read actions, if any
|
||||
if (isset($_data['action'])) {
|
||||
// Reset regex filters
|
||||
if ($_data['action'] == "reset-regex") {
|
||||
try {
|
||||
$redis->Del('F2B_REGEX');
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Rules will also be recreated on log events, but rules may seem empty for a second in the UI
|
||||
docker('post', 'netfilter-mailcow', 'restart');
|
||||
$fail_count = 0;
|
||||
$regex_result = json_decode($redis->Get('F2B_REGEX'), true);
|
||||
while (empty($regex_result) && $fail_count < 10) {
|
||||
$regex_result = json_decode($redis->Get('F2B_REGEX'), true);
|
||||
$fail_count++;
|
||||
sleep(1);
|
||||
}
|
||||
if ($fail_count >= 10) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('reset_f2b_regex')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
elseif ($_data['action'] == "edit-regex") {
|
||||
if (!empty($_data['regex'])) {
|
||||
$rule_id = 1;
|
||||
$regex_array = array();
|
||||
foreach($_data['regex'] as $regex) {
|
||||
$regex_array[$rule_id] = $regex;
|
||||
$rule_id++;
|
||||
}
|
||||
if (!empty($regex_array)) {
|
||||
$redis->Set('F2B_REGEX', json_encode($regex_array, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_modified', htmlspecialchars($network))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Start actions in dependency of network
|
||||
if (!empty($_data['network'])) {
|
||||
$networks = (array)$_data['network'];
|
||||
foreach ($networks as $network) {
|
||||
// Unban network
|
||||
if ($_data['action'] == "unban") {
|
||||
if (valid_network($network)) {
|
||||
try {
|
||||
$redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Whitelist network
|
||||
elseif ($_data['action'] == "whitelist") {
|
||||
if (empty($network)) { continue; }
|
||||
if (valid_network($network)) {
|
||||
try {
|
||||
$redis->hSet('F2B_WHITELIST', $network, 1);
|
||||
$redis->hDel('F2B_BLACKLIST', $network, 1);
|
||||
$redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $network)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Blacklist network
|
||||
elseif ($_data['action'] == "blacklist") {
|
||||
if (empty($network)) { continue; }
|
||||
if (valid_network($network) && !in_array($network, array(
|
||||
'0.0.0.0',
|
||||
'0.0.0.0/0',
|
||||
getenv('IPV4_NETWORK') . '0/24',
|
||||
getenv('IPV4_NETWORK') . '0',
|
||||
getenv('IPV6_NETWORK')
|
||||
))) {
|
||||
try {
|
||||
$redis->hSet('F2B_BLACKLIST', $network, 1);
|
||||
$redis->hDel('F2B_WHITELIST', $network, 1);
|
||||
//$response = docker('post', 'netfilter-mailcow', 'restart');
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $network)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_modified', htmlspecialchars($network))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Start default edit without specific action
|
||||
$is_now = fail2ban('get');
|
||||
if (!empty($is_now)) {
|
||||
$ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']);
|
||||
$ban_time_increment = (isset($_data['ban_time_increment']) && $_data['ban_time_increment'] == "1") ? 1 : 0;
|
||||
$max_attempts = intval((isset($_data['max_attempts'])) ? $_data['max_attempts'] : $is_now['max_attempts']);
|
||||
$max_ban_time = intval((isset($_data['max_ban_time'])) ? $_data['max_ban_time'] : $is_now['max_ban_time']);
|
||||
$retry_window = intval((isset($_data['retry_window'])) ? $_data['retry_window'] : $is_now['retry_window']);
|
||||
$netban_ipv4 = intval((isset($_data['netban_ipv4'])) ? $_data['netban_ipv4'] : $is_now['netban_ipv4']);
|
||||
$netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
|
||||
$wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
|
||||
$bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
|
||||
$manage_external = (isset($_data['manage_external'])) ? intval($_data['manage_external']) : 0;
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$f2b_options = array();
|
||||
$f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time;
|
||||
$f2b_options['ban_time_increment'] = ($ban_time_increment == 1) ? true : false;
|
||||
$f2b_options['max_ban_time'] = ($max_ban_time < 60) ? 60 : $max_ban_time;
|
||||
$f2b_options['netban_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
|
||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
|
||||
$f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;
|
||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
|
||||
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
|
||||
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
|
||||
$f2b_options['banlist_id'] = $is_now['banlist_id'];
|
||||
$f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0;
|
||||
try {
|
||||
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
||||
$redis->Del('F2B_WHITELIST');
|
||||
$redis->Del('F2B_BLACKLIST');
|
||||
if(!empty($wl)) {
|
||||
$wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
|
||||
$wl_array = array_filter($wl_array);
|
||||
if (is_array($wl_array)) {
|
||||
foreach ($wl_array as $wl_item) {
|
||||
if (valid_network($wl_item) || valid_hostname($wl_item)) {
|
||||
$redis->hSet('F2B_WHITELIST', $wl_item, 1);
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $wl_item)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!empty($bl)) {
|
||||
$bl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $bl));
|
||||
$bl_array = array_filter($bl_array);
|
||||
if (is_array($bl_array)) {
|
||||
foreach ($bl_array as $bl_item) {
|
||||
if (valid_network($bl_item) && !in_array($bl_item, array(
|
||||
'0.0.0.0',
|
||||
'0.0.0.0/0',
|
||||
getenv('IPV4_NETWORK') . '0/24',
|
||||
getenv('IPV4_NETWORK') . '0',
|
||||
getenv('IPV6_NETWORK')
|
||||
))) {
|
||||
$redis->hSet('F2B_BLACKLIST', $bl_item, 1);
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $bl_item)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'f2b_modified'
|
||||
);
|
||||
break;
|
||||
case 'banlist':
|
||||
try {
|
||||
$f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
http_response_code(500);
|
||||
return false;
|
||||
}
|
||||
if (is_array($_extra)) {
|
||||
$_extra = $_extra[0];
|
||||
}
|
||||
if ($_extra != $f2b_options['banlist_id']){
|
||||
http_response_code(404);
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($_data) {
|
||||
case 'get':
|
||||
try {
|
||||
$bl = $redis->hKeys('F2B_BLACKLIST');
|
||||
$active_bans = $redis->hKeys('F2B_ACTIVE_BANS');
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
http_response_code(500);
|
||||
return false;
|
||||
}
|
||||
$banlist = implode("\n", array_merge($bl, $active_bans));
|
||||
return $banlist;
|
||||
break;
|
||||
case 'refresh':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
return false;
|
||||
}
|
||||
|
||||
$f2b_options['banlist_id'] = uuid4();
|
||||
try {
|
||||
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||
'msg' => 'f2b_banlist_refreshed'
|
||||
);
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
183
data/web/inc/functions.fwdhost.inc.php
Executable file
183
data/web/inc/functions.fwdhost.inc.php
Executable file
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
function fwdhost($_action, $_data = null) {
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/spf.inc.php';
|
||||
global $redis;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
global $lang;
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$source = $_data['hostname'];
|
||||
$host = trim($_data['hostname']);
|
||||
$filter_spam = (isset($_data['filter_spam']) && $_data['filter_spam'] == 1) ? 1 : 0;
|
||||
if (preg_match('/^[0-9a-fA-F:\/]+$/', $host)) { // IPv6 address
|
||||
$hosts = array($host);
|
||||
}
|
||||
elseif (preg_match('/^[0-9\.\/]+$/', $host)) { // IPv4 address
|
||||
$hosts = array($host);
|
||||
}
|
||||
else {
|
||||
$hosts = get_outgoing_hosts_best_guess($host);
|
||||
}
|
||||
if (empty($hosts)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('invalid_host', htmlspecialchars($host))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
foreach ($hosts as $host) {
|
||||
try {
|
||||
$redis->hSet('WHITELISTED_FWD_HOST', $host, $source);
|
||||
if ($filter_spam == 0) {
|
||||
$redis->hSet('KEEP_SPAM', $host, 1);
|
||||
}
|
||||
elseif ($redis->hGet('KEEP_SPAM', $host)) {
|
||||
$redis->hDel('KEEP_SPAM', $host);
|
||||
}
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('forwarding_host_added', htmlspecialchars(implode(', ', (array)$hosts)))
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
global $lang;
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$fwdhosts = (array)$_data['fwdhost'];
|
||||
foreach ($fwdhosts as $fwdhost) {
|
||||
$is_now = fwdhost('details', $fwdhost);
|
||||
if (!empty($is_now)) {
|
||||
$keep_spam = (isset($_data['keep_spam'])) ? $_data['keep_spam'] : $is_now['keep_spam'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if ($keep_spam == 1) {
|
||||
$redis->hSet('KEEP_SPAM', $fwdhost, 1);
|
||||
}
|
||||
else {
|
||||
$redis->hDel('KEEP_SPAM', $fwdhost);
|
||||
}
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_modified', htmlspecialchars($fwdhost))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
$hosts = (array)$_data['forwardinghost'];
|
||||
foreach ($hosts as $host) {
|
||||
try {
|
||||
$redis->hDel('WHITELISTED_FWD_HOST', $host);
|
||||
$redis->hDel('KEEP_SPAM', $host);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('forwarding_host_removed', htmlspecialchars($host))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
return false;
|
||||
}
|
||||
$fwdhostsdata = array();
|
||||
try {
|
||||
$fwd_hosts = $redis->hGetAll('WHITELISTED_FWD_HOST');
|
||||
if (!empty($fwd_hosts)) {
|
||||
foreach ($fwd_hosts as $fwd_host => $source) {
|
||||
$keep_spam = ($redis->hGet('KEEP_SPAM', $fwd_host)) ? "yes" : "no";
|
||||
$fwdhostsdata[] = array(
|
||||
'host' => $fwd_host,
|
||||
'source' => $source,
|
||||
'keep_spam' => $keep_spam
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return $fwdhostsdata;
|
||||
break;
|
||||
case 'details':
|
||||
$fwdhostdetails = array();
|
||||
if (!isset($_data) || empty($_data)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if ($source = $redis->hGet('WHITELISTED_FWD_HOST', $_data)) {
|
||||
$fwdhostdetails['host'] = $_data;
|
||||
$fwdhostdetails['source'] = $source;
|
||||
$fwdhostdetails['keep_spam'] = ($redis->hGet('KEEP_SPAM', $_data)) ? "yes" : "no";
|
||||
}
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return $fwdhostdetails;
|
||||
break;
|
||||
}
|
||||
}
|
||||
2982
data/web/inc/functions.inc.php
Executable file
2982
data/web/inc/functions.inc.php
Executable file
File diff suppressed because it is too large
Load diff
5597
data/web/inc/functions.mailbox.inc.php
Executable file
5597
data/web/inc/functions.mailbox.inc.php
Executable file
File diff suppressed because it is too large
Load diff
121
data/web/inc/functions.mailq.inc.php
Executable file
121
data/web/inc/functions.mailq.inc.php
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
function mailq($_action, $_data = null) {
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
function process_mailq_output($returned_output, $_action, $_data) {
|
||||
if ($returned_output !== NULL) {
|
||||
if ($_action == 'cat') {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'queue_cat_success'
|
||||
)
|
||||
)));
|
||||
return $returned_output;
|
||||
}
|
||||
else {
|
||||
if (isset($returned_output['type']) && $returned_output['type'] == 'danger') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'Error: ' . $returned_output['msg']
|
||||
);
|
||||
}
|
||||
if (isset($returned_output['type']) && $returned_output['type'] == 'success') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'queue_command_success'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'unknown'
|
||||
);
|
||||
}
|
||||
}
|
||||
if ($_action == 'get') {
|
||||
$mailq_lines = docker('post', 'postfix-mailcow', 'exec', array('cmd' => 'mailq', 'task' => 'list'));
|
||||
$lines = 0;
|
||||
// Hard limit to 10000 items
|
||||
foreach (preg_split("/((\r?\n)|(\r\n?))/", $mailq_lines) as $mailq_item) if ($lines++ < 10000) {
|
||||
if (empty($mailq_item) || $mailq_item == '1') {
|
||||
continue;
|
||||
}
|
||||
$mq_line = json_decode($mailq_item, true);
|
||||
if ($mq_line !== NULL) {
|
||||
$rcpts = array();
|
||||
foreach ($mq_line['recipients'] as $rcpt) {
|
||||
if (isset($rcpt['delay_reason'])) {
|
||||
$rcpts[] = $rcpt['address'] . ' (' . $rcpt['delay_reason'] . ')';
|
||||
}
|
||||
else {
|
||||
$rcpts[] = $rcpt['address'];
|
||||
}
|
||||
}
|
||||
if (!empty($rcpts)) {
|
||||
$mq_line['recipients'] = $rcpts;
|
||||
}
|
||||
$line[] = $mq_line;
|
||||
}
|
||||
}
|
||||
if (!isset($line) || empty($line)) {
|
||||
return '[]';
|
||||
}
|
||||
else {
|
||||
return json_encode($line);
|
||||
}
|
||||
}
|
||||
elseif ($_action == 'delete') {
|
||||
if (!is_array($_data['qid'])) {
|
||||
$qids = array();
|
||||
$qids[] = $_data['qid'];
|
||||
}
|
||||
else {
|
||||
$qids = $_data['qid'];
|
||||
}
|
||||
$docker_return = docker('post', 'postfix-mailcow', 'exec', array('cmd' => 'mailq', 'task' => 'delete', 'items' => $qids));
|
||||
process_mailq_output(json_decode($docker_return, true), $_action, $_data);
|
||||
}
|
||||
elseif ($_action == 'cat') {
|
||||
if (!is_array($_data['qid'])) {
|
||||
$qids = array();
|
||||
$qids[] = $_data['qid'];
|
||||
}
|
||||
else {
|
||||
$qids = $_data['qid'];
|
||||
}
|
||||
$docker_return = docker('post', 'postfix-mailcow', 'exec', array('cmd' => 'mailq', 'task' => 'cat', 'items' => $qids));
|
||||
return process_mailq_output($docker_return, $_action, $_data);
|
||||
}
|
||||
elseif ($_action == 'edit') {
|
||||
if (in_array($_data['action'], array('hold', 'unhold', 'deliver'))) {
|
||||
if (!is_array($_data['qid'])) {
|
||||
$qids = array();
|
||||
$qids[] = $_data['qid'];
|
||||
}
|
||||
else {
|
||||
$qids = $_data['qid'];
|
||||
}
|
||||
if (!empty($qids)) {
|
||||
$docker_return = docker('post', 'postfix-mailcow', 'exec', array('cmd' => 'mailq', 'task' => $_data['action'], 'items' => $qids));
|
||||
process_mailq_output(json_decode($docker_return, true), $_action, $_data);
|
||||
}
|
||||
}
|
||||
if (in_array($_data['action'], array('flush', 'super_delete'))) {
|
||||
$docker_return = docker('post', 'postfix-mailcow', 'exec', array('cmd' => 'mailq', 'task' => $_data['action']));
|
||||
process_mailq_output(json_decode($docker_return, true), $_action, $_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
242
data/web/inc/functions.oauth2.inc.php
Executable file
242
data/web/inc/functions.oauth2.inc.php
Executable file
|
|
@ -0,0 +1,242 @@
|
|||
<?php
|
||||
function oauth2($_action, $_type, $_data = null) {
|
||||
global $pdo;
|
||||
global $redis;
|
||||
global $lang;
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
switch ($_type) {
|
||||
case 'client':
|
||||
$client_id = bin2hex(random_bytes(6));
|
||||
$client_secret = bin2hex(random_bytes(12));
|
||||
$redirect_uri = $_data['redirect_uri'];
|
||||
$scope = 'profile';
|
||||
// For future use
|
||||
// $grant_type = isset($_data['grant_type']) ? $_data['grant_type'] : 'authorization_code';
|
||||
// $scope = isset($_data['scope']) ? $_data['scope'] : 'profile';
|
||||
// if ($grant_type != "authorization_code" && $grant_type != "password") {
|
||||
// $_SESSION['return'][] = array(
|
||||
// 'type' => 'danger',
|
||||
// 'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
// 'msg' => 'access_denied'
|
||||
// );
|
||||
// return false;
|
||||
// }
|
||||
if ($scope != "profile") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => 'Invalid scope'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT 'client' FROM `oauth_clients`
|
||||
WHERE `client_id` = :client_id");
|
||||
$stmt->execute(array(':client_id' => $client_id));
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
if ($num_results != 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => 'Client ID exists'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `oauth_clients` (`client_id`, `client_secret`, `redirect_uri`, `scope`)
|
||||
VALUES (:client_id, :client_secret, :redirect_uri, :scope)");
|
||||
$stmt->execute(array(
|
||||
':client_id' => $client_id,
|
||||
':client_secret' => $client_secret,
|
||||
':redirect_uri' => $redirect_uri,
|
||||
':scope' => $scope
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => 'Added client access'
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'edit':
|
||||
switch ($_type) {
|
||||
case 'client':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$is_now = oauth2('details', 'client', $id);
|
||||
if (!empty($is_now)) {
|
||||
$redirect_uri = (!empty($_data['redirect_uri'])) ? $_data['redirect_uri'] : $is_now['redirect_uri'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (isset($_data['revoke_tokens'])) {
|
||||
$stmt = $pdo->prepare("DELETE FROM `oauth_access_tokens`
|
||||
WHERE `client_id` IN (
|
||||
SELECT `client_id` FROM `oauth_clients` WHERE `id` = :id
|
||||
)");
|
||||
$stmt->execute(array(
|
||||
':id' => $id
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `oauth_refresh_tokens`
|
||||
WHERE `client_id` IN (
|
||||
SELECT `client_id` FROM `oauth_clients` WHERE `id` = :id
|
||||
)");
|
||||
$stmt->execute(array(
|
||||
':id' => $id
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => array('object_modified', htmlspecialchars($id))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (isset($_data['renew_secret'])) {
|
||||
$client_secret = bin2hex(random_bytes(12));
|
||||
$stmt = $pdo->prepare("UPDATE `oauth_clients` SET `client_secret` = :client_secret WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':client_secret' => $client_secret,
|
||||
':id' => $id
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => array('object_modified', htmlspecialchars($id))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (empty($redirect_uri)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => 'Redirect/Callback URL cannot be empty'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("UPDATE `oauth_clients` SET
|
||||
`redirect_uri` = :redirect_uri
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $id,
|
||||
':redirect_uri' => $redirect_uri
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => array('object_modified', htmlspecialchars($id))
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
switch ($_type) {
|
||||
case 'client':
|
||||
(array)$ids = $_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
if (!is_numeric($id)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `oauth_clients`
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $id
|
||||
));
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => array('items_deleted', htmlspecialchars($id))
|
||||
);
|
||||
break;
|
||||
case 'access_token':
|
||||
(array)$access_tokens = $_data['access_token'];
|
||||
foreach ($access_tokens as $access_token) {
|
||||
if (!ctype_alnum($access_token)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `oauth_access_tokens`
|
||||
WHERE `access_token` = :access_token");
|
||||
$stmt->execute(array(
|
||||
':access_token' => $access_token
|
||||
));
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => sprintf($lang['success']['items_deleted'], implode(', ', (array)$access_tokens))
|
||||
);
|
||||
break;
|
||||
case 'refresh_token':
|
||||
(array)$refresh_tokens = $_data['refresh_token'];
|
||||
foreach ($refresh_tokens as $refresh_token) {
|
||||
if (!ctype_alnum($refresh_token)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `oauth_refresh_tokens` WHERE `refresh_token` = :refresh_token");
|
||||
$stmt->execute(array(
|
||||
':refresh_token' => $refresh_token
|
||||
));
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => sprintf($lang['success']['items_deleted'], implode(', ', (array)$refresh_tokens))
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
switch ($_type) {
|
||||
case 'clients':
|
||||
$stmt = $pdo->query("SELECT `id` FROM `oauth_clients`");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($row = array_shift($rows)) {
|
||||
$oauth_clients[] = $row['id'];
|
||||
}
|
||||
return $oauth_clients;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'details':
|
||||
switch ($_type) {
|
||||
case 'client':
|
||||
$stmt = $pdo->prepare("SELECT * FROM `oauth_clients`
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$oauth_client_details = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $oauth_client_details;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
320
data/web/inc/functions.policy.inc.php
Executable file
320
data/web/inc/functions.policy.inc.php
Executable file
|
|
@ -0,0 +1,320 @@
|
|||
<?php
|
||||
function policy($_action, $_scope, $_data = null) {
|
||||
global $pdo;
|
||||
global $redis;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
if (!isset($_SESSION['acl']['spam_policy']) || $_SESSION['acl']['spam_policy'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
switch ($_scope) {
|
||||
case 'domain':
|
||||
$object = $_data['domain'];
|
||||
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, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$object = idn_to_ascii(strtolower(trim($object)), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($_data['object_list'] == "bl") {
|
||||
$object_list = "blacklist_from";
|
||||
}
|
||||
elseif ($_data['object_list'] == "wl") {
|
||||
$object_list = "whitelist_from";
|
||||
}
|
||||
$object_from = trim(strtolower($_data['object_from']));
|
||||
if (!ctype_alnum(str_replace(array('@', '_', '.', '-', '*'), '', $object_from))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'policy_list_from_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($object_list != "blacklist_from" && $object_list != "whitelist_from") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `object` FROM `filterconf`
|
||||
WHERE (`option` = 'whitelist_from' OR `option` = 'blacklist_from')
|
||||
AND `object` = :object
|
||||
AND `value` = :object_from");
|
||||
$stmt->execute(array(':object' => $object, ':object_from' => $object_from));
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
if ($num_results != 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'policy_list_from_exists'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO `filterconf` (`object`, `option` ,`value`)
|
||||
VALUES (:object, :object_list, :object_from)");
|
||||
$stmt->execute(array(
|
||||
':object' => $object,
|
||||
':object_list' => $object_list,
|
||||
':object_from' => $object_from
|
||||
));
|
||||
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => array('domain_modified', $object)
|
||||
);
|
||||
break;
|
||||
case 'mailbox':
|
||||
$object = $_data['username'];
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($_data['object_list'] == "bl") {
|
||||
$object_list = "blacklist_from";
|
||||
}
|
||||
elseif ($_data['object_list'] == "wl") {
|
||||
$object_list = "whitelist_from";
|
||||
}
|
||||
$object_from = trim(strtolower($_data['object_from']));
|
||||
if (!ctype_alnum(str_replace(array('@', '_', '.', '-', '*'), '', $object_from))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'policy_list_from_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($object_list != "blacklist_from" && $object_list != "whitelist_from") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `object` FROM `filterconf`
|
||||
WHERE (`option` = 'whitelist_from' OR `option` = 'blacklist_from')
|
||||
AND `object` = :object
|
||||
AND `value` = :object_from");
|
||||
$stmt->execute(array(':object' => $object, ':object_from' => $object_from));
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
if ($num_results != 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'policy_list_from_exists'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `filterconf` (`object`, `option` ,`value`)
|
||||
VALUES (:object, :object_list, :object_from)");
|
||||
$stmt->execute(array(
|
||||
':object' => $object,
|
||||
':object_list' => $object_list,
|
||||
':object_from' => $object_from
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => array('mailbox_modified', $object)
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (!isset($_SESSION['acl']['spam_policy']) || $_SESSION['acl']['spam_policy'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
switch ($_scope) {
|
||||
case 'domain':
|
||||
(array)$prefids = $_data['prefid'];
|
||||
foreach ($prefids as $prefid) {
|
||||
if (!is_numeric($prefid)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `object` FROM `filterconf` WHERE `prefid` = :prefid");
|
||||
$stmt->execute(array(':prefid' => $prefid));
|
||||
$object = $stmt->fetch(PDO::FETCH_ASSOC)['object'];
|
||||
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, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$object = idn_to_ascii(strtolower(trim($object)), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :object AND `prefid` = :prefid");
|
||||
$stmt->execute(array(
|
||||
':object' => $object,
|
||||
':prefid' => $prefid
|
||||
));
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => array('mysql_error', $e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => array('item_deleted',$prefid)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'mailbox':
|
||||
if (!is_array($_data['prefid'])) {
|
||||
$prefids = array();
|
||||
$prefids[] = $_data['prefid'];
|
||||
}
|
||||
else {
|
||||
$prefids = $_data['prefid'];
|
||||
}
|
||||
foreach ($prefids as $prefid) {
|
||||
if (!is_numeric($prefid)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `object` FROM `filterconf` WHERE `prefid` = :prefid");
|
||||
$stmt->execute(array(':prefid' => $prefid));
|
||||
$object = $stmt->fetch(PDO::FETCH_ASSOC)['object'];
|
||||
if (!hasMailboxObjectAccess($_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;
|
||||
}
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :object AND `prefid` = :prefid");
|
||||
$stmt->execute(array(
|
||||
':object' => $object,
|
||||
':prefid' => $prefid
|
||||
));
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => array('mysql_error', $e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => array('items_deleted', implode(', ', (array)$prefids))
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
switch ($_scope) {
|
||||
case 'domain':
|
||||
if (!is_valid_domain_name($_data)) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
||||
return false;
|
||||
}
|
||||
$_data = idn_to_ascii(strtolower(trim($_data)), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
}
|
||||
|
||||
// WHITELIST
|
||||
$stmt = $pdo->prepare("SELECT `object`, `value`, `prefid` FROM `filterconf` WHERE `option`='whitelist_from' AND (`object` LIKE :object_mail OR `object` = :object_domain)");
|
||||
$stmt->execute(array(':object_mail' => '%@' . $_data, ':object_domain' => $_data));
|
||||
$rows['whitelist'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
// BLACKLIST
|
||||
$stmt = $pdo->prepare("SELECT `object`, `value`, `prefid` FROM `filterconf` WHERE `option`='blacklist_from' AND (`object` LIKE :object_mail OR `object` = :object_domain)");
|
||||
$stmt->execute(array(':object_mail' => '%@' . $_data, ':object_domain' => $_data));
|
||||
$rows['blacklist'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return $rows;
|
||||
break;
|
||||
case 'mailbox':
|
||||
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$_data = $_SESSION['mailcow_cc_username'];
|
||||
}
|
||||
$domain = mailbox('get', 'mailbox_details', $_data)['domain'];
|
||||
if (empty($domain)) {
|
||||
return false;
|
||||
}
|
||||
// WHITELIST
|
||||
$stmt = $pdo->prepare("SELECT `object`, `value`, `prefid` FROM `filterconf` WHERE `option`='whitelist_from' AND (`object` = :username OR `object` = :domain)");
|
||||
$stmt->execute(array(':username' => $_data, ':domain' => $domain));
|
||||
$rows['whitelist'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
// BLACKLIST
|
||||
$stmt = $pdo->prepare("SELECT `object`, `value`, `prefid` FROM `filterconf` WHERE `option`='blacklist_from' AND (`object` = :username OR `object` = :domain)");
|
||||
$stmt->execute(array(':username' => $_data, ':domain' => $domain));
|
||||
$rows['blacklist'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $rows;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
38
data/web/inc/functions.presets.inc.php
Executable file
38
data/web/inc/functions.presets.inc.php
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
function presets($_action, $_kind) {
|
||||
switch ($_action) {
|
||||
case 'get':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
return false;
|
||||
}
|
||||
$presets = array();
|
||||
$kind = strtolower(trim($_kind));
|
||||
$lang_base = 'admin';
|
||||
$presets_path = __DIR__ . '/presets/' . $kind;
|
||||
if (!in_array($kind, ['rspamd', 'sieve'], true)) {
|
||||
return array();
|
||||
}
|
||||
if ($kind === 'sieve') {
|
||||
$lang_base = 'mailbox';
|
||||
}
|
||||
foreach (glob($presets_path . '/*.yml') as $filename) {
|
||||
$presets[] = getPresetFromFilePath($filename, $lang_base);
|
||||
}
|
||||
return $presets;
|
||||
break;
|
||||
}
|
||||
return array();
|
||||
}
|
||||
function getPresetFromFilePath($filePath, $lang_base) {
|
||||
global $lang;
|
||||
$preset = Spyc::YAMLLoad($filePath);
|
||||
$preset = ['name' => basename($filePath, '.yml')] + $preset;
|
||||
/* get translated headlines */
|
||||
if (isset($preset['headline']) && strpos($preset['headline'], 'lang.') === 0) {
|
||||
$langTextName = trim(substr($preset['headline'], 5));
|
||||
if (isset($lang[$lang_base][$langTextName])) {
|
||||
$preset['headline'] = $lang[$lang_base][$langTextName];
|
||||
}
|
||||
}
|
||||
return $preset;
|
||||
}
|
||||
223
data/web/inc/functions.pushover.inc.php
Executable file
223
data/web/inc/functions.pushover.inc.php
Executable file
|
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
function pushover($_action, $_data = null) {
|
||||
global $pdo;
|
||||
switch ($_action) {
|
||||
case 'edit':
|
||||
if (!isset($_SESSION['acl']['pushover']) || $_SESSION['acl']['pushover'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!is_array($_data['username'])) {
|
||||
$usernames = array();
|
||||
$usernames[] = $_data['username'];
|
||||
}
|
||||
else {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$delete = $_data['delete'];
|
||||
if ($delete == "true") {
|
||||
$stmt = $pdo->prepare("DELETE FROM `pushover` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'pushover_settings_edited'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$is_now = pushover('get', $username);
|
||||
if (!empty($is_now)) {
|
||||
$key = (!empty($_data['key'])) ? $_data['key'] : $is_now['key'];
|
||||
$token = (!empty($_data['token'])) ? $_data['token'] : $is_now['token'];
|
||||
$senders = (isset($_data['senders'])) ? $_data['senders'] : $is_now['senders'];
|
||||
$senders_regex = (isset($_data['senders_regex'])) ? $_data['senders_regex'] : $is_now['senders_regex'];
|
||||
$title = (!empty($_data['title'])) ? $_data['title'] : $is_now['title'];
|
||||
$text = (!empty($_data['text'])) ? $_data['text'] : $is_now['text'];
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$evaluate_x_prio = (isset($_data['evaluate_x_prio'])) ? intval($_data['evaluate_x_prio']) : $is_now['evaluate_x_prio'];
|
||||
$only_x_prio = (isset($_data['only_x_prio'])) ? intval($_data['only_x_prio']) : $is_now['only_x_prio'];
|
||||
$sound = (isset($_data['sound'])) ? $_data['sound'] : $is_now['sound'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!empty($senders_regex) && !is_valid_regex($senders_regex)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'Invalid regex'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$senders = array_map('trim', preg_split( "/( |,|;|\n)/", $senders));
|
||||
foreach ($senders as $i => &$sender) {
|
||||
if (empty($sender)) {
|
||||
continue;
|
||||
}
|
||||
if (!filter_var($sender, FILTER_VALIDATE_EMAIL) === true) {
|
||||
unset($senders[$i]);
|
||||
continue;
|
||||
}
|
||||
$senders[$i] = preg_replace('/\.(?=.*?@gmail\.com$)/', '$1', $sender);
|
||||
}
|
||||
$senders = array_filter($senders);
|
||||
if (empty($senders)) { $senders = ''; }
|
||||
$senders = implode(",", (array)$senders);
|
||||
if (!ctype_alnum($key) || strlen($key) != 30) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_data),
|
||||
'msg' => 'pushover_key'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!ctype_alnum($token) || strlen($token) != 30) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_data),
|
||||
'msg' => 'pushover_token'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$po_attributes = json_encode(
|
||||
array(
|
||||
'evaluate_x_prio' => strval(intval($evaluate_x_prio)),
|
||||
'only_x_prio' => strval(intval($only_x_prio)),
|
||||
'sound' => strval($sound)
|
||||
)
|
||||
);
|
||||
$stmt = $pdo->prepare("REPLACE INTO `pushover` (`username`, `key`, `attributes`, `senders_regex`, `senders`, `token`, `title`, `text`, `active`)
|
||||
VALUES (:username, :key, :po_attributes, :senders_regex, :senders, :token, :title, :text, :active)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':key' => $key,
|
||||
':po_attributes' => $po_attributes,
|
||||
':senders_regex' => $senders_regex,
|
||||
':senders' => $senders,
|
||||
':token' => $token,
|
||||
':title' => $title,
|
||||
':text' => $text,
|
||||
':active' => $active
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'pushover_settings_edited'
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT * FROM `pushover` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $_data
|
||||
));
|
||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$data['attributes'] = json_decode($data['attributes'], true);
|
||||
if (empty($data)) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return $data;
|
||||
}
|
||||
break;
|
||||
case 'test':
|
||||
if (!isset($_SESSION['acl']['pushover']) || $_SESSION['acl']['pushover'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!is_array($_data['username'])) {
|
||||
$usernames = array();
|
||||
$usernames[] = $_data['username'];
|
||||
}
|
||||
else {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT * FROM `pushover`
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
$api_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!empty($api_data)) {
|
||||
$title = (!empty($api_data['title'])) ? $api_data['title'] : 'Mail';
|
||||
$text = (!empty($api_data['text'])) ? $api_data['text'] : 'You\'ve got mail 📧';
|
||||
curl_setopt_array($ch = curl_init(), array(
|
||||
CURLOPT_URL => "https://api.pushover.net/1/users/validate.json",
|
||||
CURLOPT_POSTFIELDS => array(
|
||||
"token" => $api_data['token'],
|
||||
"user" => $api_data['key']
|
||||
),
|
||||
CURLOPT_SAFE_UPLOAD => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
));
|
||||
$result = curl_exec($ch);
|
||||
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($httpcode == 200) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => sprintf('Pushover API OK (%d): %s', $httpcode, $result)
|
||||
);
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => sprintf('Pushover API ERR (%d): %s', $httpcode, $result)
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data),
|
||||
'msg' => 'pushover_credentials_missing'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
841
data/web/inc/functions.quarantine.inc.php
Executable file
841
data/web/inc/functions.quarantine.inc.php
Executable file
|
|
@ -0,0 +1,841 @@
|
|||
<?php
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
function quarantine($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $redis;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
switch ($_action) {
|
||||
case 'quick_delete':
|
||||
// Dont return results, just log
|
||||
$hash = trim($_data);
|
||||
if (preg_match("/^([a-f0-9]{64})$/", $hash) === false) {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
||||
WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash
|
||||
AND user_acl.quarantine = 1
|
||||
AND rcpt IN (SELECT username FROM mailbox)');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row['id']) || !is_numeric($row['id'])) {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE id = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $row['id']
|
||||
));
|
||||
}
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('item_deleted', $row['id'])
|
||||
)
|
||||
)));
|
||||
break;
|
||||
case 'quick_release':
|
||||
// Dont return results, just log
|
||||
$hash = trim($_data);
|
||||
if (preg_match("/^([a-f0-9]{64})$/", $hash) === false) {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
||||
WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash
|
||||
AND `user_acl`.`quarantine` = 1
|
||||
AND `username` IN (SELECT `username` FROM `mailbox`)');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row['id']) || !is_numeric($row['id'])) {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare('SELECT `msg`, `qid`, `sender`, `rcpt` FROM `quarantine` WHERE `id` = :id');
|
||||
$stmt->execute(array(':id' => $row['id']));
|
||||
$detail_row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$sender = !empty($detail_row['sender']) ? $detail_row['sender'] : 'sender-unknown@rspamd';
|
||||
if (!empty(gethostbynamel('postfix-mailcow'))) {
|
||||
$postfix = 'postfix-mailcow';
|
||||
}
|
||||
if (!empty(gethostbynamel('postfix'))) {
|
||||
$postfix = 'postfix';
|
||||
}
|
||||
else {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('release_send_failed', 'Cannot determine Postfix host')
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$release_format = $redis->Get('Q_RELEASE_FORMAT');
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
if ($release_format == 'attachment') {
|
||||
try {
|
||||
$mail = new PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
$mail->SMTPDebug = 0;
|
||||
$mail->SMTPOptions = array(
|
||||
'ssl' => array(
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'allow_self_signed' => true
|
||||
)
|
||||
);
|
||||
if (!empty(gethostbynamel('postfix-mailcow'))) {
|
||||
$postfix = 'postfix-mailcow';
|
||||
}
|
||||
if (!empty(gethostbynamel('postfix'))) {
|
||||
$postfix = 'postfix';
|
||||
}
|
||||
else {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('release_send_failed', 'Cannot determine Postfix host')
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
$mail->Host = $postfix;
|
||||
$mail->Port = 590;
|
||||
$mail->setFrom($sender);
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Subject = sprintf($lang['quarantine']['release_subject'], $detail_row['qid']);
|
||||
$mail->addAddress($detail_row['rcpt']);
|
||||
$mail->IsHTML(false);
|
||||
$msg_tmpf = tempnam("/tmp", $detail_row['qid']);
|
||||
file_put_contents($msg_tmpf, $detail_row['msg']);
|
||||
$mail->addAttachment($msg_tmpf, $detail_row['qid'] . '.eml');
|
||||
$mail->Body = sprintf($lang['quarantine']['release_body']);
|
||||
$mail->send();
|
||||
unlink($msg_tmpf);
|
||||
}
|
||||
catch (phpmailerException $e) {
|
||||
unlink($msg_tmpf);
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('release_send_failed', $e->errorMessage())
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
elseif ($release_format == 'raw') {
|
||||
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $detail_row['msg']);
|
||||
$postfix_talk = array(
|
||||
array('220', 'HELO quarantine' . chr(10)),
|
||||
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
||||
array('250', 'RCPT TO: ' . $detail_row['rcpt'] . chr(10)),
|
||||
array('250', 'DATA' . chr(10)),
|
||||
array('354', $detail_row['msg'] . chr(10) . '.' . chr(10)),
|
||||
array('250', 'QUIT' . chr(10)),
|
||||
array('221', '')
|
||||
);
|
||||
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
|
||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||
if (!$smtp_connection) {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'Cannot connect to Postfix'
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
for ($i=0; $i < count($postfix_talk); $i++) {
|
||||
$smtp_resource = fgets($smtp_connection, 256);
|
||||
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
|
||||
$ret = substr($smtp_resource, 0, 3);
|
||||
$ret = (empty($ret)) ? '-' : $ret;
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'Postfix returned SMTP code ' . $smtp_resource . ', expected ' . $postfix_talk[$i][0]
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
if ($postfix_talk[$i][1] !== '') {
|
||||
fputs($smtp_connection, $postfix_talk[$i][1]);
|
||||
}
|
||||
}
|
||||
fclose($smtp_connection);
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE id = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $row['id']
|
||||
));
|
||||
}
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('item_released', $hash)
|
||||
)
|
||||
)));
|
||||
break;
|
||||
case 'delete':
|
||||
if (!is_array($_data['id'])) {
|
||||
$ids = array();
|
||||
$ids[] = $_data['id'];
|
||||
}
|
||||
else {
|
||||
$ids = $_data['id'];
|
||||
}
|
||||
if (!isset($_SESSION['acl']['quarantine']) || $_SESSION['acl']['quarantine'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
foreach ($ids as $id) {
|
||||
if (!is_numeric($id)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT `rcpt` FROM `quarantine` WHERE `id` = :id');
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt']) && $_SESSION['mailcow_cc_role'] != 'admin') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $id
|
||||
));
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('item_deleted', $id)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'edit':
|
||||
if (!isset($_SESSION['acl']['quarantine']) || $_SESSION['acl']['quarantine'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Edit settings
|
||||
if ($_data['action'] == 'settings') {
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$retention_size = $_data['retention_size'];
|
||||
if ($_data['release_format'] == 'attachment' || $_data['release_format'] == 'raw') {
|
||||
$release_format = $_data['release_format'];
|
||||
}
|
||||
else {
|
||||
$release_format = 'raw';
|
||||
}
|
||||
$max_size = $_data['max_size'];
|
||||
if ($_data['max_score'] == "") {
|
||||
$max_score = '';
|
||||
}
|
||||
else {
|
||||
$max_score = floatval($_data['max_score']);
|
||||
}
|
||||
$max_age = intval($_data['max_age']);
|
||||
$subject = $_data['subject'];
|
||||
if (!filter_var($_data['bcc'], FILTER_VALIDATE_EMAIL)) {
|
||||
$bcc = '';
|
||||
}
|
||||
else {
|
||||
$bcc = $_data['bcc'];
|
||||
}
|
||||
if (!filter_var($_data['redirect'], FILTER_VALIDATE_EMAIL)) {
|
||||
$redirect = '';
|
||||
}
|
||||
else {
|
||||
$redirect = $_data['redirect'];
|
||||
}
|
||||
if (!filter_var($_data['sender'], FILTER_VALIDATE_EMAIL)) {
|
||||
$sender = '';
|
||||
}
|
||||
else {
|
||||
$sender = $_data['sender'];
|
||||
}
|
||||
$html = $_data['html_tmpl'];
|
||||
if ($max_age <= 0) {
|
||||
$max_age = 365;
|
||||
}
|
||||
$exclude_domains = (array)$_data['exclude_domains'];
|
||||
try {
|
||||
$redis->Set('Q_RETENTION_SIZE', intval($retention_size));
|
||||
$redis->Set('Q_MAX_SIZE', intval($max_size));
|
||||
$redis->Set('Q_MAX_SCORE', $max_score);
|
||||
$redis->Set('Q_MAX_AGE', $max_age);
|
||||
$redis->Set('Q_EXCLUDE_DOMAINS', json_encode($exclude_domains));
|
||||
$redis->Set('Q_RELEASE_FORMAT', $release_format);
|
||||
$redis->Set('Q_SENDER', $sender);
|
||||
$redis->Set('Q_BCC', $bcc);
|
||||
$redis->Set('Q_REDIRECT', $redirect);
|
||||
$redis->Set('Q_SUBJ', $subject);
|
||||
$redis->Set('Q_HTML', $html);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'saved_settings'
|
||||
);
|
||||
}
|
||||
// Release item
|
||||
elseif ($_data['action'] == 'release' || $_data['action'] == 'learnham') {
|
||||
if (!is_array($_data['id'])) {
|
||||
$ids = array();
|
||||
$ids[] = $_data['id'];
|
||||
}
|
||||
else {
|
||||
$ids = $_data['id'];
|
||||
}
|
||||
foreach ($ids as $id) {
|
||||
if (!is_numeric($id)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT `msg`, `action`, `qid`, `sender`, `rcpt` FROM `quarantine` WHERE `id` = :id');
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt']) && $_SESSION['mailcow_cc_role'] != 'admin' || empty($row['rcpt'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$sender = !empty($row['sender']) ? $row['sender'] : 'sender-unknown@rspamd';
|
||||
if (!empty(gethostbynamel('postfix-mailcow'))) {
|
||||
$postfix = 'postfix-mailcow';
|
||||
}
|
||||
if (!empty(gethostbynamel('postfix'))) {
|
||||
$postfix = 'postfix';
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('release_send_failed', 'Cannot determine Postfix host')
|
||||
);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$release_format = $redis->Get('Q_RELEASE_FORMAT');
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($release_format == 'attachment') {
|
||||
try {
|
||||
$mail = new PHPMailer(true);
|
||||
$mail->isSMTP();
|
||||
$mail->SMTPDebug = 0;
|
||||
$mail->SMTPOptions = array(
|
||||
'ssl' => array(
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'allow_self_signed' => true
|
||||
)
|
||||
);
|
||||
if (!empty(gethostbynamel('postfix-mailcow'))) {
|
||||
$postfix = 'postfix-mailcow';
|
||||
}
|
||||
if (!empty(gethostbynamel('postfix'))) {
|
||||
$postfix = 'postfix';
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('release_send_failed', 'Cannot determine Postfix host')
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$mail->Host = $postfix;
|
||||
$mail->Port = 590;
|
||||
$mail->setFrom($sender);
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Subject = sprintf($lang['quarantine']['release_subject'], $row['qid']);
|
||||
$mail->addAddress($row['rcpt']);
|
||||
$mail->IsHTML(false);
|
||||
$msg_tmpf = tempnam("/tmp", $row['qid']);
|
||||
file_put_contents($msg_tmpf, $row['msg']);
|
||||
$mail->addAttachment($msg_tmpf, $row['qid'] . '.eml');
|
||||
$mail->Body = sprintf($lang['quarantine']['release_body']);
|
||||
$mail->send();
|
||||
unlink($msg_tmpf);
|
||||
}
|
||||
catch (phpmailerException $e) {
|
||||
unlink($msg_tmpf);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('release_send_failed', $e->errorMessage())
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
elseif ($release_format == 'raw') {
|
||||
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $row['msg']);
|
||||
$postfix_talk = array(
|
||||
array('220', 'HELO quarantine' . chr(10)),
|
||||
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
||||
array('250', 'RCPT TO: ' . $row['rcpt'] . chr(10)),
|
||||
array('250', 'DATA' . chr(10)),
|
||||
array('354', str_replace("\n.", '', $row['msg']) . chr(10) . '.' . chr(10)),
|
||||
array('250', 'QUIT' . chr(10)),
|
||||
array('221', '')
|
||||
);
|
||||
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
|
||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||
if (!$smtp_connection) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'Cannot connect to Postfix'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
for ($i=0; $i < count($postfix_talk); $i++) {
|
||||
$smtp_resource = fgets($smtp_connection, 256);
|
||||
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
|
||||
$ret = substr($smtp_resource, 0, 3);
|
||||
$ret = (empty($ret)) ? '-' : $ret;
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'Postfix returned SMTP code ' . $smtp_resource . ', expected ' . $postfix_talk[$i][0]
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($postfix_talk[$i][1] !== '') {
|
||||
fputs($smtp_connection, $postfix_talk[$i][1]);
|
||||
}
|
||||
}
|
||||
fclose($smtp_connection);
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $id
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('item_released', $id)
|
||||
);
|
||||
// Item was released and deleted from quarantine, now learning ham
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 1);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain'));
|
||||
curl_setopt($curl, CURLOPT_URL,"http://rspamd/learnham");
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
|
||||
$response = curl_exec($curl);
|
||||
if (!curl_errno($curl)) {
|
||||
$response = json_decode($response, true);
|
||||
if (isset($response['error'])) {
|
||||
if (stripos($response['error'], 'already learned') === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('ham_learn_error', $response['error'])
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
curl_close($curl);
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 1);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain', 'Flag: 11'));
|
||||
curl_setopt($curl, CURLOPT_URL,"http://rspamd/fuzzydel");
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
|
||||
// It is most likely not a spam hash, so we ignore any error/warning response
|
||||
// $response = curl_exec($curl);
|
||||
if (!curl_errno($curl)) {
|
||||
curl_close($curl);
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 1);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain', 'Flag: 13'));
|
||||
curl_setopt($curl, CURLOPT_URL,"http://rspamd/fuzzyadd");
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
|
||||
$response = curl_exec($curl);
|
||||
curl_exec($curl);
|
||||
if (!curl_errno($curl)) {
|
||||
$response = json_decode($response, true);
|
||||
if (isset($response['error'])) {
|
||||
if (stripos($response['error'], 'No content to generate fuzzy') === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('fuzzy_learn_error', $response['error'])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
curl_close($curl);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('learned_ham', $id)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
curl_close($curl);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('ham_learn_error', 'Curl: ' . curl_strerror(curl_errno($curl)))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
curl_close($curl);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('ham_learn_error', 'unknown')
|
||||
);
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('ham_learn_error', 'Curl: ' . curl_strerror(curl_errno($curl)))
|
||||
);
|
||||
curl_close($curl);
|
||||
continue;
|
||||
}
|
||||
curl_close($curl);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('ham_learn_error', 'unknown')
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
elseif ($_data['action'] == 'learnspam') {
|
||||
if (!is_array($_data['id'])) {
|
||||
$ids = array();
|
||||
$ids[] = $_data['id'];
|
||||
}
|
||||
else {
|
||||
$ids = $_data['id'];
|
||||
}
|
||||
foreach ($ids as $id) {
|
||||
if (!is_numeric($id)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT `msg`, `rcpt`, `action` FROM `quarantine` WHERE `id` = :id');
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt']) && $_SESSION['mailcow_cc_role'] != 'admin' || empty($row['rcpt'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $id
|
||||
));
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 1);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain'));
|
||||
curl_setopt($curl, CURLOPT_URL,"http://rspamd/learnspam");
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
|
||||
$response = curl_exec($curl);
|
||||
if (!curl_errno($curl)) {
|
||||
$response = json_decode($response, true);
|
||||
if (isset($response['error'])) {
|
||||
if (stripos($response['error'], 'already learned') === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('spam_learn_error', $response['error'])
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
curl_close($curl);
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 1);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain', 'Flag: 13'));
|
||||
curl_setopt($curl, CURLOPT_URL,"http://rspamd/fuzzydel");
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
|
||||
// It is most likely not a spam hash, so we ignore any error/warning response
|
||||
// $response = curl_exec($curl);
|
||||
if (!curl_errno($curl)) {
|
||||
curl_close($curl);
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_POST, 1);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: text/plain', 'Flag: 11'));
|
||||
curl_setopt($curl, CURLOPT_URL,"http://rspamd/fuzzyadd");
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $row['msg']);
|
||||
$response = curl_exec($curl);
|
||||
curl_exec($curl);
|
||||
if (!curl_errno($curl)) {
|
||||
$response = json_decode($response, true);
|
||||
if (isset($response['error'])) {
|
||||
if (stripos($response['error'], 'No content to generate fuzzy') === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('fuzzy_learn_error', $response['error'])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
curl_close($curl);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('qlearn_spam', $id)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
curl_close($curl);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('spam_learn_error', 'Curl: ' . curl_strerror(curl_errno($curl)))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
curl_close($curl);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('spam_learn_error', 'unknown')
|
||||
);
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('spam_learn_error', 'Curl: ' . curl_strerror(curl_errno($curl)))
|
||||
);
|
||||
curl_close($curl);
|
||||
continue;
|
||||
}
|
||||
curl_close($curl);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('spam_learn_error', 'unknown')
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
case 'get':
|
||||
if ($_SESSION['mailcow_cc_role'] == "user") {
|
||||
$stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `score`, `rcpt`, `sender`, `action`, UNIX_TIMESTAMP(`created`) AS `created`, `notified` FROM `quarantine` WHERE `rcpt` = :mbox');
|
||||
$stmt->execute(array(':mbox' => $_SESSION['mailcow_cc_username']));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$q_meta[] = $row;
|
||||
}
|
||||
}
|
||||
elseif ($_SESSION['mailcow_cc_role'] == "admin") {
|
||||
$stmt = $pdo->query('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `score`, `rcpt`, `sender`, `action`, UNIX_TIMESTAMP(`created`) AS `created`, `notified` FROM `quarantine`');
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$q_meta[] = $row;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
|
||||
foreach ($domains as $domain) {
|
||||
$stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `score`, `rcpt`, `sender`, `action`, UNIX_TIMESTAMP(`created`) AS `created`, `notified` FROM `quarantine` WHERE `rcpt` REGEXP :domain');
|
||||
$stmt->execute(array(':domain' => '@' . $domain . '$'));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$q_meta[] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $q_meta;
|
||||
break;
|
||||
case 'settings':
|
||||
try {
|
||||
if ($_SESSION['mailcow_cc_role'] == "admin") {
|
||||
$settings['exclude_domains'] = json_decode($redis->Get('Q_EXCLUDE_DOMAINS'), true);
|
||||
}
|
||||
$settings['max_size'] = $redis->Get('Q_MAX_SIZE');
|
||||
$settings['max_score'] = $redis->Get('Q_MAX_SCORE');
|
||||
$settings['max_age'] = $redis->Get('Q_MAX_AGE');
|
||||
$settings['retention_size'] = $redis->Get('Q_RETENTION_SIZE');
|
||||
$settings['release_format'] = $redis->Get('Q_RELEASE_FORMAT');
|
||||
$settings['subject'] = $redis->Get('Q_SUBJ');
|
||||
$settings['sender'] = $redis->Get('Q_SENDER');
|
||||
$settings['bcc'] = $redis->Get('Q_BCC');
|
||||
$settings['redirect'] = $redis->Get('Q_REDIRECT');
|
||||
$settings['html_tmpl'] = htmlspecialchars($redis->Get('Q_HTML'));
|
||||
if (empty($settings['html_tmpl'])) {
|
||||
$settings['html_tmpl'] = htmlspecialchars(file_get_contents("/tpls/quarantine.tpl"));
|
||||
}
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return $settings;
|
||||
break;
|
||||
case 'details':
|
||||
if (!is_numeric($_data) || empty($_data)) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `id`= :id');
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt']) || $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
return $row;
|
||||
}
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
break;
|
||||
case 'hash_details':
|
||||
$hash = trim($_data);
|
||||
if (preg_match("/^([a-f0-9]{64})$/", $hash) === false) {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
)
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
break;
|
||||
}
|
||||
}
|
||||
150
data/web/inc/functions.quota_notification.inc.php
Executable file
150
data/web/inc/functions.quota_notification.inc.php
Executable file
|
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
function quota_notification($_action, $_data = null) {
|
||||
global $redis;
|
||||
$_data_log = $_data;
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
switch ($_action) {
|
||||
case 'edit':
|
||||
$retention_size = $_data['retention_size'];
|
||||
if ($_data['release_format'] == 'attachment' || $_data['release_format'] == 'raw') {
|
||||
$release_format = $_data['release_format'];
|
||||
}
|
||||
else {
|
||||
$release_format = 'raw';
|
||||
}
|
||||
$subject = $_data['subject'];
|
||||
$sender = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $_data['sender']);
|
||||
if (filter_var($sender, FILTER_VALIDATE_EMAIL) === false) {
|
||||
$sender = '';
|
||||
}
|
||||
$html = $_data['html_tmpl'];
|
||||
try {
|
||||
$redis->Set('QW_SENDER', $sender);
|
||||
$redis->Set('QW_SUBJ', $subject);
|
||||
$redis->Set('QW_HTML', $html);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'saved_settings'
|
||||
);
|
||||
break;
|
||||
case 'get':
|
||||
try {
|
||||
$settings['subject'] = $redis->Get('QW_SUBJ');
|
||||
$settings['sender'] = $redis->Get('QW_SENDER');
|
||||
$settings['html_tmpl'] = htmlspecialchars($redis->Get('QW_HTML'));
|
||||
if (empty($settings['html_tmpl'])) {
|
||||
$settings['html_tmpl'] = htmlspecialchars(file_get_contents("/tpls/quota.tpl"));
|
||||
}
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return $settings;
|
||||
break;
|
||||
}
|
||||
}
|
||||
function quota_notification_bcc($_action, $_data = null) {
|
||||
global $redis;
|
||||
$_data_log = $_data;
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
switch ($_action) {
|
||||
case 'edit':
|
||||
$domain = $_data['domain'];
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$active = intval($_data['active']);
|
||||
$bcc_rcpts = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['bcc_rcpt']));
|
||||
foreach ($bcc_rcpts as $i => &$rcpt) {
|
||||
$rcpt = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $rcpt);
|
||||
if (!empty($rcpt) && filter_var($rcpt, FILTER_VALIDATE_EMAIL) === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('goto_invalid', htmlspecialchars($rcpt))
|
||||
);
|
||||
unset($bcc_rcpts[$i]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$bcc_rcpts = array_unique($bcc_rcpts);
|
||||
$bcc_rcpts = array_filter($bcc_rcpts);
|
||||
if (empty($bcc_rcpts)) {
|
||||
$active = 0;
|
||||
|
||||
}
|
||||
try {
|
||||
$redis->hSet('QW_BCC', $domain, json_encode(array('bcc_rcpts' => $bcc_rcpts, 'active' => $active)));
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'saved_settings'
|
||||
);
|
||||
break;
|
||||
case 'get':
|
||||
$domain = $_data;
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return json_decode($redis->hGet('QW_BCC', $domain), true);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
242
data/web/inc/functions.ratelimit.inc.php
Executable file
242
data/web/inc/functions.ratelimit.inc.php
Executable file
|
|
@ -0,0 +1,242 @@
|
|||
<?php
|
||||
function ratelimit($_action, $_scope, $_data = null) {
|
||||
global $redis;
|
||||
$_data_log = $_data;
|
||||
switch ($_action) {
|
||||
case 'edit':
|
||||
if (!isset($_SESSION['acl']['ratelimit']) || $_SESSION['acl']['ratelimit'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
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', 'd'))) {
|
||||
$_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('rl_saved', $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', 'd'))) {
|
||||
$_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('rl_saved', $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;
|
||||
case 'delete':
|
||||
$data['hash'] = $_data;
|
||||
if ($_SESSION['mailcow_cc_role'] != 'admin' || !preg_match('/^RL[0-9A-Za-z=]+$/i', trim($data['hash']))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$data_rllog = $redis->lRange('RL_LOG', 0, -1);
|
||||
if ($data_rllog) {
|
||||
foreach ($data_rllog as $json_line) {
|
||||
if (preg_match('/' . $data['hash'] . '/i', $json_line)) {
|
||||
$redis->lRem('RL_LOG', $json_line, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($redis->type($data['hash']) == Redis::REDIS_HASH) {
|
||||
$redis->delete($data['hash']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'hash_deleted'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
'msg' => 'hash_not_found'
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
220
data/web/inc/functions.rspamd.inc.php
Executable file
220
data/web/inc/functions.rspamd.inc.php
Executable file
|
|
@ -0,0 +1,220 @@
|
|||
<?php
|
||||
function rsettings($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$content = $_data['content'];
|
||||
$desc = $_data['desc'];
|
||||
$active = intval($_data['active']);
|
||||
if (empty($content)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'map_content_empty'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `settingsmap` (`content`, `desc`, `active`)
|
||||
VALUES (:content, :desc, :active)");
|
||||
$stmt->execute(array(
|
||||
':content' => $content,
|
||||
':desc' => $desc,
|
||||
':active' => $active
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'settings_map_added'
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$is_now = rsettings('details', $id);
|
||||
if (!empty($is_now)) {
|
||||
$content = (!empty($_data['content'])) ? $_data['content'] : $is_now['content'];
|
||||
$desc = (!empty($_data['desc'])) ? $_data['desc'] : $is_now['desc'];
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('settings_map_invalid', $id)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$content = trim($content);
|
||||
$stmt = $pdo->prepare("UPDATE `settingsmap` SET
|
||||
`content` = :content,
|
||||
`desc` = :desc,
|
||||
`active` = :active
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':content' => $content,
|
||||
':desc' => $desc,
|
||||
':active' => $active,
|
||||
':id' => $id
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_modified', htmlspecialchars(implode(',', $ids)))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$stmt = $pdo->prepare("DELETE FROM `settingsmap` WHERE `id`= :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('settings_map_removed', htmlspecialchars($id))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
return false;
|
||||
}
|
||||
$settingsmaps = array();
|
||||
$stmt = $pdo->query("SELECT `id`, `desc`, `active` FROM `settingsmap`");
|
||||
$settingsmaps = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $settingsmaps;
|
||||
break;
|
||||
case 'details':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
|
||||
return false;
|
||||
}
|
||||
$settingsmapdata = array();
|
||||
$stmt = $pdo->prepare("SELECT `id`,
|
||||
`desc`,
|
||||
`content`,
|
||||
`active`
|
||||
FROM `settingsmap`
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$settingsmapdata = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $settingsmapdata;
|
||||
break;
|
||||
}
|
||||
}
|
||||
function rspamd_maps($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
global $RSPAMD_MAPS;
|
||||
$_data_log = $_data;
|
||||
switch ($_action) {
|
||||
case 'edit':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, '-'),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$maps = (array)$_data['map'];
|
||||
$valid_maps = array();
|
||||
foreach ($maps as $map) {
|
||||
$is_valid = false;
|
||||
foreach ($RSPAMD_MAPS as $rspamd_map_type) {
|
||||
if (in_array($map, $rspamd_map_type)) {
|
||||
$is_valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($is_valid) {
|
||||
array_push($valid_maps, $map);
|
||||
} else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, '-'),
|
||||
'msg' => array('global_map_invalid', $map)
|
||||
);
|
||||
}
|
||||
}
|
||||
foreach ($valid_maps as $map) {
|
||||
try {
|
||||
if (file_exists('/rspamd_custom_maps/' . $map)) {
|
||||
$map_content = trim($_data['rspamd_map_data']);
|
||||
$map_handle = fopen('/rspamd_custom_maps/' . $map, 'w');
|
||||
if (!$map_handle) {
|
||||
throw new Exception($lang['danger']['file_open_error']);
|
||||
}
|
||||
fwrite($map_handle, $map_content . PHP_EOL);
|
||||
fclose($map_handle);
|
||||
sleep(1.5);
|
||||
touch('/rspamd_custom_maps/' . $map);
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, '-'),
|
||||
'msg' => array('global_map_write_error', htmlspecialchars($map), htmlspecialchars($e->getMessage()))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, '-'),
|
||||
'msg' => array('object_modified', htmlspecialchars($map))
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
function rspamd_actions() {
|
||||
if (isset($_SESSION["mailcow_cc_role"]) && $_SESSION["mailcow_cc_role"] == "admin") {
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
|
||||
curl_setopt($curl, CURLOPT_URL,"http://rspamd/stat");
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
$data = curl_exec($curl);
|
||||
if ($data) {
|
||||
$return = array();
|
||||
$stats_array = json_decode($data, true)['actions'];
|
||||
$stats_array['soft reject'] = $stats_array['soft reject'] + $stats_array['greylist'];
|
||||
unset($stats_array['greylist']);
|
||||
foreach ($stats_array as $action => $count) {
|
||||
$return[] = array($action, $count);
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
172
data/web/inc/functions.tls_policy_maps.inc.php
Executable file
172
data/web/inc/functions.tls_policy_maps.inc.php
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
function tls_policy_maps($_action, $_data = null, $attr = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
return false;
|
||||
}
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
$dest = idn_to_ascii(trim($_data['dest']), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
$policy = strtolower(trim($_data['policy']));
|
||||
$parameters = (isset($_data['parameters']) && !empty($_data['parameters'])) ? $_data['parameters'] : '';
|
||||
if (empty($dest) || in_array($dest, array('.', '*', '@'))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'tls_policy_map_dest_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!empty($parameters)) {
|
||||
foreach (explode(' ', $parameters) as $parameter) {
|
||||
if (!preg_match('/(.+)\=(.+)/i', $parameter)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'tls_policy_map_parameter_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
$active = intval($_data['active']);
|
||||
$tls_policy_maps = tls_policy_maps('get');
|
||||
foreach ($tls_policy_maps as $tls_policy_map) {
|
||||
if (tls_policy_maps('details', $tls_policy_map)['dest'] == $dest) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('tls_policy_map_entry_exists', htmlspecialchars($dest))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `tls_policy_override` (`dest`, `policy`, `parameters`, `active`) VALUES
|
||||
(:dest, :policy, :parameters, :active)");
|
||||
$stmt->execute(array(
|
||||
':dest' => $dest,
|
||||
':policy' => $policy,
|
||||
':parameters' => $parameters,
|
||||
':active' => $active
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('tls_policy_map_entry_saved', htmlspecialchars($dest))
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$is_now = tls_policy_maps('details', $id);
|
||||
if (!empty($is_now)) {
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$dest = (!empty($_data['dest'])) ? $_data['dest'] : $is_now['dest'];
|
||||
$policy = (!empty($_data['policy'])) ? $_data['policy'] : $is_now['policy'];
|
||||
$parameters = (isset($_data['parameters'])) ? $_data['parameters'] : $is_now['parameters'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (empty($dest) || in_array($dest, array('.', '*', '@'))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'tls_policy_map_dest_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!empty($parameters)) {
|
||||
foreach (explode(' ', $parameters) as $parameter) {
|
||||
if (!preg_match('/(.+)\=(.+)/i', $parameter)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => 'tls_policy_map_parameter_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
$tls_policy_maps = tls_policy_maps('get');
|
||||
foreach ($tls_policy_maps as $tls_policy_map) {
|
||||
if ($tls_policy_map == $id) { continue; }
|
||||
if (tls_policy_maps('details', $tls_policy_map)['dest'] == $dest) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('recipient_map_entry_exists', htmlspecialchars($dest))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare("UPDATE `tls_policy_override` SET
|
||||
`dest` = :dest,
|
||||
`policy` = :policy,
|
||||
`parameters` = :parameters,
|
||||
`active` = :active
|
||||
WHERE `id`= :id");
|
||||
$stmt->execute(array(
|
||||
':dest' => $dest,
|
||||
':policy' => $policy,
|
||||
':parameters' => $parameters,
|
||||
':active' => $active,
|
||||
':id' => $id
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('tls_policy_map_entry_saved', htmlspecialchars($dest))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'details':
|
||||
$mapdata = array();
|
||||
$id = intval($_data);
|
||||
$stmt = $pdo->prepare("SELECT `id`,
|
||||
`dest`,
|
||||
`policy`,
|
||||
`parameters`,
|
||||
`active` AS `active`,
|
||||
`created`,
|
||||
`modified` FROM `tls_policy_override`
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$mapdata = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $mapdata;
|
||||
break;
|
||||
case 'get':
|
||||
$mapdata = array();
|
||||
$all_items = array();
|
||||
$id = intval($_data);
|
||||
$stmt = $pdo->query("SELECT `id` FROM `tls_policy_override`");
|
||||
$all_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($all_items as $i) {
|
||||
$mapdata[] = $i['id'];
|
||||
}
|
||||
$all_items = null;
|
||||
return $mapdata;
|
||||
break;
|
||||
case 'delete':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
if (!is_numeric($id)) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `tls_policy_override` WHERE `id`= :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||
'msg' => array('tls_policy_map_entry_deleted', htmlspecialchars($id))
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
508
data/web/inc/functions.transports.inc.php
Executable file
508
data/web/inc/functions.transports.inc.php
Executable file
|
|
@ -0,0 +1,508 @@
|
|||
<?php
|
||||
function relayhost($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$hostname = trim($_data['hostname']);
|
||||
$username = str_replace(':', '\:', trim($_data['username']));
|
||||
$password = str_replace(':', '\:', trim($_data['password']));
|
||||
if (empty($hostname)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('invalid_host', htmlspecialchars($host))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$stmt = $pdo->prepare("INSERT INTO `relayhosts` (`hostname`, `username` ,`password`, `active`)
|
||||
VALUES (:hostname, :username, :password, :active)");
|
||||
$stmt->execute(array(
|
||||
':hostname' => $hostname,
|
||||
':username' => $username,
|
||||
':password' => str_replace(':', '\:', $password),
|
||||
':active' => '1'
|
||||
));
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('mysql_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('relayhost_added', htmlspecialchars(implode(', ', (array)$hosts)))
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$is_now = relayhost('details', $id);
|
||||
if (!empty($is_now)) {
|
||||
$hostname = (!empty($_data['hostname'])) ? trim($_data['hostname']) : $is_now['hostname'];
|
||||
$username = (isset($_data['username'])) ? trim($_data['username']) : $is_now['username'];
|
||||
$password = (isset($_data['password'])) ? trim($_data['password']) : $is_now['password'];
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('relayhost_invalid', $id)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE `relayhosts` SET
|
||||
`hostname` = :hostname,
|
||||
`username` = :username,
|
||||
`password` = :password,
|
||||
`active` = :active
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $id,
|
||||
':hostname' => $hostname,
|
||||
':username' => $username,
|
||||
':password' => $password,
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('mysql_error', $e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_modified', htmlspecialchars(implode(', ', (array)$hostnames)))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM `relayhosts` WHERE `id`= :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$stmt = $pdo->prepare("UPDATE `domain` SET `relayhost` = '0' WHERE `relayhost`= :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('mysql_error', $e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('relayhost_removed', htmlspecialchars($id))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
return false;
|
||||
}
|
||||
$relayhosts = array();
|
||||
$stmt = $pdo->query("SELECT `id`, `hostname`, `username`, `active` FROM `relayhosts`");
|
||||
$relayhosts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $relayhosts;
|
||||
break;
|
||||
case 'details':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
|
||||
return false;
|
||||
}
|
||||
$relayhostdata = array();
|
||||
$stmt = $pdo->prepare("SELECT `id`,
|
||||
`hostname`,
|
||||
`username`,
|
||||
`password`,
|
||||
`active`,
|
||||
CONCAT(LEFT(`password`, 3), '...') AS `password_short`
|
||||
FROM `relayhosts`
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$relayhostdata = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!empty($relayhostdata)) {
|
||||
$stmt = $pdo->prepare("SELECT GROUP_CONCAT(`domain` SEPARATOR ', ') AS `used_by_domains` FROM `domain` WHERE `relayhost` = :id");
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$used_by_domains = $stmt->fetch(PDO::FETCH_ASSOC)['used_by_domains'];
|
||||
$used_by_domains = (empty($used_by_domains)) ? '' : $used_by_domains;
|
||||
$relayhostdata['used_by_domains'] = $used_by_domains;
|
||||
$stmt = $pdo->prepare("SELECT GROUP_CONCAT(`username` SEPARATOR ', ') AS `used_by_mailboxes` FROM `mailbox` WHERE JSON_VALUE(`attributes`, '$.relayhost') = :id");
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$used_by_mailboxes = $stmt->fetch(PDO::FETCH_ASSOC)['used_by_mailboxes'];
|
||||
$used_by_mailboxes = (empty($used_by_mailboxes)) ? '' : $used_by_mailboxes;
|
||||
$relayhostdata['used_by_mailboxes'] = $used_by_mailboxes;
|
||||
}
|
||||
return $relayhostdata;
|
||||
break;
|
||||
}
|
||||
}
|
||||
function transport($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$destinations = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['destination']));
|
||||
$active = intval($_data['active']);
|
||||
$is_mx_based = intval($_data['is_mx_based']);
|
||||
$nexthop = trim($_data['nexthop']);
|
||||
if (filter_var($nexthop, FILTER_VALIDATE_IP)) {
|
||||
$nexthop = '[' . $nexthop . ']';
|
||||
}
|
||||
preg_match('/\[(.+)\].*/', $nexthop, $next_hop_matches);
|
||||
$next_hop_clean = (isset($next_hop_matches[1])) ? $next_hop_matches[1] : $nexthop;
|
||||
$username = str_replace(':', '\:', trim($_data['username']));
|
||||
$password = str_replace(':', '\:', trim($_data['password']));
|
||||
if (empty($nexthop)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('invalid_nexthop')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$transports = transport('get');
|
||||
if (!empty($transports)) {
|
||||
foreach ($transports as $transport) {
|
||||
$transport_data = transport('details', $transport['id']);
|
||||
$existing_nh[] = $transport_data['nexthop'];
|
||||
preg_match('/\[(.+)\].*/', $transport_data['nexthop'], $existing_clean_nh[]);
|
||||
if (($transport_data['nexthop'] == $nexthop || $transport_data['nexthop'] == $next_hop_clean) && $transport_data['username'] != $username) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'invalid_nexthop_authenticated'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
foreach ($destinations as $d_ix => &$dest) {
|
||||
if (empty($dest)) {
|
||||
unset($destinations[$d_ix]);
|
||||
continue;
|
||||
}
|
||||
if ($transport_data['destination'] == $dest) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('transport_dest_exists', $dest)
|
||||
);
|
||||
unset($destinations[$d_ix]);
|
||||
continue;
|
||||
}
|
||||
// ".domain" is a valid destination, "..domain" is not
|
||||
if ($is_mx_based == 0 && (empty($dest) || (is_valid_domain_name(preg_replace('/^' . preg_quote('.', '/') . '/', '', $dest)) === false && $dest != '*' && filter_var($dest, FILTER_VALIDATE_EMAIL) === false))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('invalid_destination', $dest)
|
||||
);
|
||||
unset($destinations[$d_ix]);
|
||||
continue;
|
||||
}
|
||||
if ($is_mx_based == 1 && (empty($dest) || @preg_match('/' . $dest . '/', null) === false)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('invalid_destination', $dest)
|
||||
);
|
||||
unset($destinations[$d_ix]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$destinations = array_filter(array_values(array_unique($destinations)));
|
||||
if (empty($destinations)) { return false; }
|
||||
if (isset($next_hop_matches[1])) {
|
||||
if ($existing_nh !== null && in_array($next_hop_clean, $existing_nh)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('next_hop_interferes', $next_hop_clean, $nexthop)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach ($existing_clean_nh as $existing_clean_nh_each) {
|
||||
if ($existing_clean_nh_each[1] == $nexthop) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('next_hop_interferes_any', $nexthop)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($destinations as $insert_dest) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `transports` (`nexthop`, `destination`, `is_mx_based`, `username` , `password`, `active`)
|
||||
VALUES (:nexthop, :destination, :is_mx_based, :username, :password, :active)");
|
||||
$stmt->execute(array(
|
||||
':nexthop' => $nexthop,
|
||||
':destination' => $insert_dest,
|
||||
':is_mx_based' => $is_mx_based,
|
||||
':username' => $username,
|
||||
':password' => str_replace(':', '\:', $password),
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
$stmt = $pdo->prepare("UPDATE `transports` SET
|
||||
`username` = :username,
|
||||
`password` = :password
|
||||
WHERE `nexthop` = :nexthop");
|
||||
$stmt->execute(array(
|
||||
':nexthop' => $nexthop,
|
||||
':username' => $username,
|
||||
':password' => $password
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('relayhost_added', htmlspecialchars(implode(', ', (array)$hosts)))
|
||||
);
|
||||
break;
|
||||
case 'edit':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$is_now = transport('details', $id);
|
||||
if (!empty($is_now)) {
|
||||
$destination = (!empty($_data['destination'])) ? trim($_data['destination']) : $is_now['destination'];
|
||||
$nexthop = (!empty($_data['nexthop'])) ? trim($_data['nexthop']) : $is_now['nexthop'];
|
||||
$username = (isset($_data['username'])) ? trim($_data['username']) : $is_now['username'];
|
||||
$password = (isset($_data['password'])) ? trim($_data['password']) : $is_now['password'];
|
||||
$is_mx_based = (isset($_data['is_mx_based']) && $_data['is_mx_based'] != '') ? intval($_data['is_mx_based']) : $is_now['is_mx_based'];
|
||||
$active = (isset($_data['active']) && $_data['active'] != '') ? intval($_data['active']) : $is_now['active'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('relayhost_invalid', $id)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
preg_match('/\[(.+)\].*/', $nexthop, $next_hop_matches);
|
||||
if (filter_var($nexthop, FILTER_VALIDATE_IP)) {
|
||||
$nexthop = '[' . $nexthop . ']';
|
||||
}
|
||||
$next_hop_clean = (isset($next_hop_matches[1])) ? $next_hop_matches[1] : $nexthop;
|
||||
$transports = transport('get');
|
||||
if (!empty($transports)) {
|
||||
foreach ($transports as $transport) {
|
||||
$transport_data = transport('details', $transport['id']);
|
||||
if ($transport['id'] == $id) {
|
||||
continue;
|
||||
}
|
||||
$existing_nh[] = $transport_data['nexthop'];
|
||||
preg_match('/\[(.+)\].*/', $transport_data['nexthop'], $existing_clean_nh[]);
|
||||
if ($transport_data['destination'] == $destination) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'transport_dest_exists'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($is_mx_based == 0 && (empty($destination) || (is_valid_domain_name(preg_replace('/^' . preg_quote('.', '/') . '/', '', $destination)) === false && $destination != '*' && filter_var($destination, FILTER_VALIDATE_EMAIL) === false))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('invalid_destination', $destination)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($is_mx_based == 1 && (empty($destination) || @preg_match('/' . $destination . '/', null) === false)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('invalid_destination', $destination)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (isset($next_hop_matches[1])) {
|
||||
if ($existing_nh !== null && in_array($next_hop_clean, $existing_nh)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('next_hop_interferes', $next_hop_clean, $nexthop)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach ($existing_clean_nh as $existing_clean_nh_each) {
|
||||
if ($existing_clean_nh_each[1] == $nexthop) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('next_hop_interferes_any', $nexthop)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($username)) {
|
||||
$password = '';
|
||||
}
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE `transports` SET
|
||||
`destination` = :destination,
|
||||
`is_mx_based` = :is_mx_based,
|
||||
`nexthop` = :nexthop,
|
||||
`username` = :username,
|
||||
`password` = :password,
|
||||
`active` = :active
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $id,
|
||||
':destination' => $destination,
|
||||
':is_mx_based' => $is_mx_based,
|
||||
':nexthop' => $nexthop,
|
||||
':username' => $username,
|
||||
':password' => $password,
|
||||
':active' => $active
|
||||
));
|
||||
$stmt = $pdo->prepare("UPDATE `transports` SET
|
||||
`username` = :username,
|
||||
`password` = :password
|
||||
WHERE `nexthop` = :nexthop");
|
||||
$stmt->execute(array(
|
||||
':nexthop' => $nexthop,
|
||||
':username' => $username,
|
||||
':password' => $password
|
||||
));
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('mysql_error', $e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_modified', htmlspecialchars(implode(', ', (array)$hostnames)))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM `transports` WHERE `id`= :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('mysql_error', $e)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('relayhost_removed', htmlspecialchars($id))
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'get':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
return false;
|
||||
}
|
||||
$transports = array();
|
||||
$stmt = $pdo->query("SELECT `id`, `is_mx_based`, `destination`, `nexthop`, `username` FROM `transports`");
|
||||
$transports = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $transports;
|
||||
break;
|
||||
case 'details':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
|
||||
return false;
|
||||
}
|
||||
$transportdata = array();
|
||||
$stmt = $pdo->prepare("SELECT `id`,
|
||||
`is_mx_based`,
|
||||
`destination`,
|
||||
`nexthop`,
|
||||
`username`,
|
||||
`password`,
|
||||
`active`,
|
||||
CONCAT(LEFT(`password`, 3), '...') AS `password_short`
|
||||
FROM `transports`
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$transportdata = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $transportdata;
|
||||
break;
|
||||
}
|
||||
}
|
||||
56
data/web/inc/header.inc.php
Executable file
56
data/web/inc/header.inc.php
Executable file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
// CSS
|
||||
if (preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) {
|
||||
$css_minifier->add('/web/css/site/mailbox.css');
|
||||
}
|
||||
if (preg_match("/admin/i", $_SERVER['REQUEST_URI'])) {
|
||||
$css_minifier->add('/web/css/site/admin.css');
|
||||
}
|
||||
if (preg_match("/user/i", $_SERVER['REQUEST_URI'])) {
|
||||
$css_minifier->add('/web/css/site/user.css');
|
||||
}
|
||||
if (preg_match("/edit/i", $_SERVER['REQUEST_URI'])) {
|
||||
$css_minifier->add('/web/css/site/edit.css');
|
||||
}
|
||||
if (preg_match("/(quarantine|qhandler)/i", $_SERVER['REQUEST_URI'])) {
|
||||
$css_minifier->add('/web/css/site/quarantine.css');
|
||||
}
|
||||
if (preg_match("/debug/i", $_SERVER['REQUEST_URI'])) {
|
||||
$css_minifier->add('/web/css/site/debug.css');
|
||||
}
|
||||
if ($_SERVER['REQUEST_URI'] == '/') {
|
||||
$css_minifier->add('/web/css/site/index.css');
|
||||
}
|
||||
|
||||
$hash = $css_minifier->getDataHash();
|
||||
$CSSPath = '/tmp/' . $hash . '.css';
|
||||
if(!file_exists($CSSPath)) {
|
||||
$css_minifier->minify($CSSPath);
|
||||
cleanupCSS($hash);
|
||||
}
|
||||
|
||||
$globalVariables = [
|
||||
'mailcow_hostname' => getenv('MAILCOW_HOSTNAME'),
|
||||
'mailcow_locale' => @$_SESSION['mailcow_locale'],
|
||||
'mailcow_cc_role' => @$_SESSION['mailcow_cc_role'],
|
||||
'mailcow_cc_username' => @$_SESSION['mailcow_cc_username'],
|
||||
'is_master' => preg_match('/y|yes/i', getenv('MASTER')),
|
||||
'dual_login' => @$_SESSION['dual-login'],
|
||||
'ui_texts' => $UI_TEXTS,
|
||||
'css_path' => '/cache/'.basename($CSSPath),
|
||||
'logo' => customize('get', 'main_logo'),
|
||||
'logo_dark' => customize('get', 'main_logo_dark'),
|
||||
'available_languages' => $AVAILABLE_LANGUAGES,
|
||||
'lang' => $lang,
|
||||
'skip_sogo' => (getenv('SKIP_SOGO') == 'y'),
|
||||
'allow_admin_email_login' => (getenv('ALLOW_ADMIN_EMAIL_LOGIN') == 'n'),
|
||||
'mailcow_apps' => $MAILCOW_APPS,
|
||||
'app_links' => customize('get', 'app_links'),
|
||||
'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
|
||||
'uri' => $_SERVER['REQUEST_URI'],
|
||||
];
|
||||
|
||||
foreach ($globalVariables as $globalVariableName => $globalVariableValue) {
|
||||
$twig->addGlobal($globalVariableName, $globalVariableValue);
|
||||
}
|
||||
1522
data/web/inc/init_db.inc.php
Executable file
1522
data/web/inc/init_db.inc.php
Executable file
File diff suppressed because it is too large
Load diff
18
data/web/inc/lib/CSSminifierExtended.php
Executable file
18
data/web/inc/lib/CSSminifierExtended.php
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
use MatthiasMullie\Minify\CSS;
|
||||
|
||||
class CSSminifierExtended extends CSS {
|
||||
|
||||
public function getDataHash() {
|
||||
return sha1(json_encode($this->accessProtected($this,'data')));
|
||||
}
|
||||
|
||||
private function accessProtected($obj, $prop) {
|
||||
$reflection = new ReflectionClass($obj);
|
||||
$property = $reflection->getProperty($prop);
|
||||
$property->setAccessible(true);
|
||||
return $property->getValue($obj);
|
||||
}
|
||||
|
||||
}
|
||||
18
data/web/inc/lib/JSminifierExtended.php
Executable file
18
data/web/inc/lib/JSminifierExtended.php
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
use MatthiasMullie\Minify\JS;
|
||||
|
||||
class JSminifierExtended extends JS {
|
||||
|
||||
public function getDataHash() {
|
||||
return sha1(json_encode($this->accessProtected($this,'data')));
|
||||
}
|
||||
|
||||
private function accessProtected($obj, $prop) {
|
||||
$reflection = new ReflectionClass($obj);
|
||||
$property = $reflection->getProperty($prop);
|
||||
$property->setAccessible(true);
|
||||
return $property->getValue($obj);
|
||||
}
|
||||
|
||||
}
|
||||
171
data/web/inc/lib/WebAuthn/Attestation/AttestationObject.php
Executable file
171
data/web/inc/lib/WebAuthn/Attestation/AttestationObject.php
Executable file
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
|
||||
namespace lbuchs\WebAuthn\Attestation;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
use lbuchs\WebAuthn\CBOR\CborDecoder;
|
||||
use lbuchs\WebAuthn\Binary\ByteBuffer;
|
||||
|
||||
/**
|
||||
* @author Lukas Buchs
|
||||
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||
*/
|
||||
class AttestationObject {
|
||||
private $_authenticatorData;
|
||||
private $_attestationFormat;
|
||||
private $_attestationFormatName;
|
||||
|
||||
public function __construct($binary , $allowedFormats) {
|
||||
$enc = CborDecoder::decode($binary);
|
||||
// validation
|
||||
if (!\is_array($enc) || !\array_key_exists('fmt', $enc) || !is_string($enc['fmt'])) {
|
||||
throw new WebAuthnException('invalid attestation format', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('attStmt', $enc) || !\is_array($enc['attStmt'])) {
|
||||
throw new WebAuthnException('invalid attestation format (attStmt not available)', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('authData', $enc) || !\is_object($enc['authData']) || !($enc['authData'] instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('invalid attestation format (authData not available)', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$this->_authenticatorData = new AuthenticatorData($enc['authData']->getBinaryString());
|
||||
$this->_attestationFormatName = $enc['fmt'];
|
||||
|
||||
// Format ok?
|
||||
if (!in_array($this->_attestationFormatName, $allowedFormats)) {
|
||||
throw new WebAuthnException('invalid atttestation format: ' . $this->_attestationFormatName, WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
|
||||
switch ($this->_attestationFormatName) {
|
||||
case 'android-key': $this->_attestationFormat = new Format\AndroidKey($enc, $this->_authenticatorData); break;
|
||||
case 'android-safetynet': $this->_attestationFormat = new Format\AndroidSafetyNet($enc, $this->_authenticatorData); break;
|
||||
case 'apple': $this->_attestationFormat = new Format\Apple($enc, $this->_authenticatorData); break;
|
||||
case 'fido-u2f': $this->_attestationFormat = new Format\U2f($enc, $this->_authenticatorData); break;
|
||||
case 'none': $this->_attestationFormat = new Format\None($enc, $this->_authenticatorData); break;
|
||||
case 'packed': $this->_attestationFormat = new Format\Packed($enc, $this->_authenticatorData); break;
|
||||
case 'tpm': $this->_attestationFormat = new Format\Tpm($enc, $this->_authenticatorData); break;
|
||||
default: throw new WebAuthnException('invalid attestation format: ' . $enc['fmt'], WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the attestation format name
|
||||
* @return string
|
||||
*/
|
||||
public function getAttestationFormatName() {
|
||||
return $this->_attestationFormatName;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the attestation public key in PEM format
|
||||
* @return AuthenticatorData
|
||||
*/
|
||||
public function getAuthenticatorData() {
|
||||
return $this->_authenticatorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the certificate chain as PEM
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCertificateChain() {
|
||||
return $this->_attestationFormat->getCertificateChain();
|
||||
}
|
||||
|
||||
/**
|
||||
* return the certificate issuer as string
|
||||
* @return string
|
||||
*/
|
||||
public function getCertificateIssuer() {
|
||||
$pem = $this->getCertificatePem();
|
||||
$issuer = '';
|
||||
if ($pem) {
|
||||
$certInfo = \openssl_x509_parse($pem);
|
||||
if (\is_array($certInfo) && \array_key_exists('issuer', $certInfo) && \is_array($certInfo['issuer'])) {
|
||||
|
||||
$cn = $certInfo['issuer']['CN'] ?? '';
|
||||
$o = $certInfo['issuer']['O'] ?? '';
|
||||
$ou = $certInfo['issuer']['OU'] ?? '';
|
||||
|
||||
if ($cn) {
|
||||
$issuer .= $cn;
|
||||
}
|
||||
if ($issuer && ($o || $ou)) {
|
||||
$issuer .= ' (' . trim($o . ' ' . $ou) . ')';
|
||||
} else {
|
||||
$issuer .= trim($o . ' ' . $ou);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $issuer;
|
||||
}
|
||||
|
||||
/**
|
||||
* return the certificate subject as string
|
||||
* @return string
|
||||
*/
|
||||
public function getCertificateSubject() {
|
||||
$pem = $this->getCertificatePem();
|
||||
$subject = '';
|
||||
if ($pem) {
|
||||
$certInfo = \openssl_x509_parse($pem);
|
||||
if (\is_array($certInfo) && \array_key_exists('subject', $certInfo) && \is_array($certInfo['subject'])) {
|
||||
|
||||
$cn = $certInfo['subject']['CN'] ?? '';
|
||||
$o = $certInfo['subject']['O'] ?? '';
|
||||
$ou = $certInfo['subject']['OU'] ?? '';
|
||||
|
||||
if ($cn) {
|
||||
$subject .= $cn;
|
||||
}
|
||||
if ($subject && ($o || $ou)) {
|
||||
$subject .= ' (' . trim($o . ' ' . $ou) . ')';
|
||||
} else {
|
||||
$subject .= trim($o . ' ' . $ou);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the key certificate in PEM format
|
||||
* @return string
|
||||
*/
|
||||
public function getCertificatePem() {
|
||||
return $this->_attestationFormat->getCertificatePem();
|
||||
}
|
||||
|
||||
/**
|
||||
* checks validity of the signature
|
||||
* @param string $clientDataHash
|
||||
* @return bool
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function validateAttestation($clientDataHash) {
|
||||
return $this->_attestationFormat->validateAttestation($clientDataHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the certificate against root certificates
|
||||
* @param array $rootCas
|
||||
* @return boolean
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function validateRootCertificate($rootCas) {
|
||||
return $this->_attestationFormat->validateRootCertificate($rootCas);
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if the RpId-Hash is valid
|
||||
* @param string$rpIdHash
|
||||
* @return bool
|
||||
*/
|
||||
public function validateRpIdHash($rpIdHash) {
|
||||
return $rpIdHash === $this->_authenticatorData->getRpIdHash();
|
||||
}
|
||||
}
|
||||
423
data/web/inc/lib/WebAuthn/Attestation/AuthenticatorData.php
Executable file
423
data/web/inc/lib/WebAuthn/Attestation/AuthenticatorData.php
Executable file
|
|
@ -0,0 +1,423 @@
|
|||
<?php
|
||||
|
||||
namespace lbuchs\WebAuthn\Attestation;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
use lbuchs\WebAuthn\CBOR\CborDecoder;
|
||||
use lbuchs\WebAuthn\Binary\ByteBuffer;
|
||||
|
||||
/**
|
||||
* @author Lukas Buchs
|
||||
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||
*/
|
||||
class AuthenticatorData {
|
||||
protected $_binary;
|
||||
protected $_rpIdHash;
|
||||
protected $_flags;
|
||||
protected $_signCount;
|
||||
protected $_attestedCredentialData;
|
||||
protected $_extensionData;
|
||||
|
||||
|
||||
|
||||
// Cose encoded keys
|
||||
private static $_COSE_KTY = 1;
|
||||
private static $_COSE_ALG = 3;
|
||||
|
||||
// Cose EC2 ES256 P-256 curve
|
||||
private static $_COSE_CRV = -1;
|
||||
private static $_COSE_X = -2;
|
||||
private static $_COSE_Y = -3;
|
||||
|
||||
// Cose RSA PS256
|
||||
private static $_COSE_N = -1;
|
||||
private static $_COSE_E = -2;
|
||||
|
||||
private static $_EC2_TYPE = 2;
|
||||
private static $_EC2_ES256 = -7;
|
||||
private static $_EC2_P256 = 1;
|
||||
|
||||
private static $_RSA_TYPE = 3;
|
||||
private static $_RSA_RS256 = -257;
|
||||
|
||||
/**
|
||||
* Parsing the authenticatorData binary.
|
||||
* @param string $binary
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function __construct($binary) {
|
||||
if (!\is_string($binary) || \strlen($binary) < 37) {
|
||||
throw new WebAuthnException('Invalid authenticatorData input', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
$this->_binary = $binary;
|
||||
|
||||
// Read infos from binary
|
||||
// https://www.w3.org/TR/webauthn/#sec-authenticator-data
|
||||
|
||||
// RP ID
|
||||
$this->_rpIdHash = \substr($binary, 0, 32);
|
||||
|
||||
// flags (1 byte)
|
||||
$flags = \unpack('Cflags', \substr($binary, 32, 1))['flags'];
|
||||
$this->_flags = $this->_readFlags($flags);
|
||||
|
||||
// signature counter: 32-bit unsigned big-endian integer.
|
||||
$this->_signCount = \unpack('Nsigncount', \substr($binary, 33, 4))['signcount'];
|
||||
|
||||
$offset = 37;
|
||||
// https://www.w3.org/TR/webauthn/#sec-attested-credential-data
|
||||
if ($this->_flags->attestedDataIncluded) {
|
||||
$this->_attestedCredentialData = $this->_readAttestData($binary, $offset);
|
||||
}
|
||||
|
||||
if ($this->_flags->extensionDataIncluded) {
|
||||
$this->_readExtensionData(\substr($binary, $offset));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticator Attestation Globally Unique Identifier, a unique number
|
||||
* that identifies the model of the authenticator (not the specific instance
|
||||
* of the authenticator)
|
||||
* The aaguid may be 0 if the user is using a old u2f device and/or if
|
||||
* the browser is using the fido-u2f format.
|
||||
* @return string
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function getAAGUID() {
|
||||
if (!($this->_attestedCredentialData instanceof \stdClass)) {
|
||||
throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
return $this->_attestedCredentialData->aaguid;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the authenticatorData as binary
|
||||
* @return string
|
||||
*/
|
||||
public function getBinary() {
|
||||
return $this->_binary;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the credentialId
|
||||
* @return string
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function getCredentialId() {
|
||||
if (!($this->_attestedCredentialData instanceof \stdClass)) {
|
||||
throw new WebAuthnException('credential id not included in authenticator data', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
return $this->_attestedCredentialData->credentialId;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the public key in PEM format
|
||||
* @return string
|
||||
*/
|
||||
public function getPublicKeyPem() {
|
||||
$der = null;
|
||||
switch ($this->_attestedCredentialData->credentialPublicKey->kty) {
|
||||
case self::$_EC2_TYPE: $der = $this->_getEc2Der(); break;
|
||||
case self::$_RSA_TYPE: $der = $this->_getRsaDer(); break;
|
||||
default: throw new WebAuthnException('invalid key type', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$pem = '-----BEGIN PUBLIC KEY-----' . "\n";
|
||||
$pem .= \chunk_split(\base64_encode($der), 64, "\n");
|
||||
$pem .= '-----END PUBLIC KEY-----' . "\n";
|
||||
return $pem;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the public key in U2F format
|
||||
* @return string
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function getPublicKeyU2F() {
|
||||
if (!($this->_attestedCredentialData instanceof \stdClass)) {
|
||||
throw new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
return "\x04" . // ECC uncompressed
|
||||
$this->_attestedCredentialData->credentialPublicKey->x .
|
||||
$this->_attestedCredentialData->credentialPublicKey->y;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the SHA256 hash of the relying party id (=hostname)
|
||||
* @return string
|
||||
*/
|
||||
public function getRpIdHash() {
|
||||
return $this->_rpIdHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the sign counter
|
||||
* @return int
|
||||
*/
|
||||
public function getSignCount() {
|
||||
return $this->_signCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if the user is present
|
||||
* @return boolean
|
||||
*/
|
||||
public function getUserPresent() {
|
||||
return $this->_flags->userPresent;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if the user is verified
|
||||
* @return boolean
|
||||
*/
|
||||
public function getUserVerified() {
|
||||
return $this->_flags->userVerified;
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
// PRIVATE
|
||||
// -----------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns DER encoded EC2 key
|
||||
* @return string
|
||||
*/
|
||||
private function _getEc2Der() {
|
||||
return $this->_der_sequence(
|
||||
$this->_der_sequence(
|
||||
$this->_der_oid("\x2A\x86\x48\xCE\x3D\x02\x01") . // OID 1.2.840.10045.2.1 ecPublicKey
|
||||
$this->_der_oid("\x2A\x86\x48\xCE\x3D\x03\x01\x07") // 1.2.840.10045.3.1.7 prime256v1
|
||||
) .
|
||||
$this->_der_bitString($this->getPublicKeyU2F())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns DER encoded RSA key
|
||||
* @return string
|
||||
*/
|
||||
private function _getRsaDer() {
|
||||
return $this->_der_sequence(
|
||||
$this->_der_sequence(
|
||||
$this->_der_oid("\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01") . // OID 1.2.840.113549.1.1.1 rsaEncryption
|
||||
$this->_der_nullValue()
|
||||
) .
|
||||
$this->_der_bitString(
|
||||
$this->_der_sequence(
|
||||
$this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->n) .
|
||||
$this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->e)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* reads the flags from flag byte
|
||||
* @param string $binFlag
|
||||
* @return \stdClass
|
||||
*/
|
||||
private function _readFlags($binFlag) {
|
||||
$flags = new \stdClass();
|
||||
|
||||
$flags->bit_0 = !!($binFlag & 1);
|
||||
$flags->bit_1 = !!($binFlag & 2);
|
||||
$flags->bit_2 = !!($binFlag & 4);
|
||||
$flags->bit_3 = !!($binFlag & 8);
|
||||
$flags->bit_4 = !!($binFlag & 16);
|
||||
$flags->bit_5 = !!($binFlag & 32);
|
||||
$flags->bit_6 = !!($binFlag & 64);
|
||||
$flags->bit_7 = !!($binFlag & 128);
|
||||
|
||||
// named flags
|
||||
$flags->userPresent = $flags->bit_0;
|
||||
$flags->userVerified = $flags->bit_2;
|
||||
$flags->attestedDataIncluded = $flags->bit_6;
|
||||
$flags->extensionDataIncluded = $flags->bit_7;
|
||||
return $flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* read attested data
|
||||
* @param string $binary
|
||||
* @param int $endOffset
|
||||
* @return \stdClass
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function _readAttestData($binary, &$endOffset) {
|
||||
$attestedCData = new \stdClass();
|
||||
if (\strlen($binary) <= 55) {
|
||||
throw new WebAuthnException('Attested data should be present but is missing', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
// The AAGUID of the authenticator
|
||||
$attestedCData->aaguid = \substr($binary, 37, 16);
|
||||
|
||||
//Byte length L of Credential ID, 16-bit unsigned big-endian integer.
|
||||
$length = \unpack('nlength', \substr($binary, 53, 2))['length'];
|
||||
$attestedCData->credentialId = \substr($binary, 55, $length);
|
||||
|
||||
// set end offset
|
||||
$endOffset = 55 + $length;
|
||||
|
||||
// extract public key
|
||||
$attestedCData->credentialPublicKey = $this->_readCredentialPublicKey($binary, 55 + $length, $endOffset);
|
||||
|
||||
return $attestedCData;
|
||||
}
|
||||
|
||||
/**
|
||||
* reads COSE key-encoded elliptic curve public key in EC2 format
|
||||
* @param string $binary
|
||||
* @param int $endOffset
|
||||
* @return \stdClass
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function _readCredentialPublicKey($binary, $offset, &$endOffset) {
|
||||
$enc = CborDecoder::decodeInPlace($binary, $offset, $endOffset);
|
||||
|
||||
// COSE key-encoded elliptic curve public key in EC2 format
|
||||
$credPKey = new \stdClass();
|
||||
$credPKey->kty = $enc[self::$_COSE_KTY];
|
||||
$credPKey->alg = $enc[self::$_COSE_ALG];
|
||||
|
||||
switch ($credPKey->alg) {
|
||||
case self::$_EC2_ES256: $this->_readCredentialPublicKeyES256($credPKey, $enc); break;
|
||||
case self::$_RSA_RS256: $this->_readCredentialPublicKeyRS256($credPKey, $enc); break;
|
||||
}
|
||||
|
||||
return $credPKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* extract ES256 informations from cose
|
||||
* @param \stdClass $credPKey
|
||||
* @param \stdClass $enc
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function _readCredentialPublicKeyES256(&$credPKey, $enc) {
|
||||
$credPKey->crv = $enc[self::$_COSE_CRV];
|
||||
$credPKey->x = $enc[self::$_COSE_X] instanceof ByteBuffer ? $enc[self::$_COSE_X]->getBinaryString() : null;
|
||||
$credPKey->y = $enc[self::$_COSE_Y] instanceof ByteBuffer ? $enc[self::$_COSE_Y]->getBinaryString() : null;
|
||||
unset ($enc);
|
||||
|
||||
// Validation
|
||||
if ($credPKey->kty !== self::$_EC2_TYPE) {
|
||||
throw new WebAuthnException('public key not in EC2 format', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
if ($credPKey->alg !== self::$_EC2_ES256) {
|
||||
throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
if ($credPKey->crv !== self::$_EC2_P256) {
|
||||
throw new WebAuthnException('curve not P-256', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
if (\strlen($credPKey->x) !== 32) {
|
||||
throw new WebAuthnException('Invalid X-coordinate', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
if (\strlen($credPKey->y) !== 32) {
|
||||
throw new WebAuthnException('Invalid Y-coordinate', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* extract RS256 informations from COSE
|
||||
* @param \stdClass $credPKey
|
||||
* @param \stdClass $enc
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function _readCredentialPublicKeyRS256(&$credPKey, $enc) {
|
||||
$credPKey->n = $enc[self::$_COSE_N] instanceof ByteBuffer ? $enc[self::$_COSE_N]->getBinaryString() : null;
|
||||
$credPKey->e = $enc[self::$_COSE_E] instanceof ByteBuffer ? $enc[self::$_COSE_E]->getBinaryString() : null;
|
||||
unset ($enc);
|
||||
|
||||
// Validation
|
||||
if ($credPKey->kty !== self::$_RSA_TYPE) {
|
||||
throw new WebAuthnException('public key not in RSA format', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
if ($credPKey->alg !== self::$_RSA_RS256) {
|
||||
throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
if (\strlen($credPKey->n) !== 256) {
|
||||
throw new WebAuthnException('Invalid RSA modulus', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
if (\strlen($credPKey->e) !== 3) {
|
||||
throw new WebAuthnException('Invalid RSA public exponent', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* reads cbor encoded extension data.
|
||||
* @param string $binary
|
||||
* @return array
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function _readExtensionData($binary) {
|
||||
$ext = CborDecoder::decode($binary);
|
||||
if (!\is_array($ext)) {
|
||||
throw new WebAuthnException('invalid extension data', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
return $ext;
|
||||
}
|
||||
|
||||
|
||||
// ---------------
|
||||
// DER functions
|
||||
// ---------------
|
||||
|
||||
private function _der_length($len) {
|
||||
if ($len < 128) {
|
||||
return \chr($len);
|
||||
}
|
||||
$lenBytes = '';
|
||||
while ($len > 0) {
|
||||
$lenBytes = \chr($len % 256) . $lenBytes;
|
||||
$len = \intdiv($len, 256);
|
||||
}
|
||||
return \chr(0x80 | \strlen($lenBytes)) . $lenBytes;
|
||||
}
|
||||
|
||||
private function _der_sequence($contents) {
|
||||
return "\x30" . $this->_der_length(\strlen($contents)) . $contents;
|
||||
}
|
||||
|
||||
private function _der_oid($encoded) {
|
||||
return "\x06" . $this->_der_length(\strlen($encoded)) . $encoded;
|
||||
}
|
||||
|
||||
private function _der_bitString($bytes) {
|
||||
return "\x03" . $this->_der_length(\strlen($bytes) + 1) . "\x00" . $bytes;
|
||||
}
|
||||
|
||||
private function _der_nullValue() {
|
||||
return "\x05\x00";
|
||||
}
|
||||
|
||||
private function _der_unsignedInteger($bytes) {
|
||||
$len = \strlen($bytes);
|
||||
|
||||
// Remove leading zero bytes
|
||||
for ($i = 0; $i < ($len - 1); $i++) {
|
||||
if (\ord($bytes[$i]) !== 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($i !== 0) {
|
||||
$bytes = \substr($bytes, $i);
|
||||
}
|
||||
|
||||
// If most significant bit is set, prefix with another zero to prevent it being seen as negative number
|
||||
if ((\ord($bytes[0]) & 0x80) !== 0) {
|
||||
$bytes = "\x00" . $bytes;
|
||||
}
|
||||
|
||||
return "\x02" . $this->_der_length(\strlen($bytes)) . $bytes;
|
||||
}
|
||||
}
|
||||
96
data/web/inc/lib/WebAuthn/Attestation/Format/AndroidKey.php
Executable file
96
data/web/inc/lib/WebAuthn/Attestation/Format/AndroidKey.php
Executable file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace lbuchs\WebAuthn\Attestation\Format;
|
||||
use lbuchs\WebAuthn\Attestation\AuthenticatorData;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
use lbuchs\WebAuthn\Binary\ByteBuffer;
|
||||
|
||||
class AndroidKey extends FormatBase {
|
||||
private $_alg;
|
||||
private $_signature;
|
||||
private $_x5c;
|
||||
|
||||
public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
|
||||
parent::__construct($AttestionObject, $authenticatorData);
|
||||
|
||||
// check u2f data
|
||||
$attStmt = $this->_attestationObject['attStmt'];
|
||||
|
||||
if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
|
||||
throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) < 1) {
|
||||
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$this->_alg = $attStmt['alg'];
|
||||
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||
$this->_x5c = $attStmt['x5c'][0]->getBinaryString();
|
||||
|
||||
if (count($attStmt['x5c']) > 1) {
|
||||
for ($i=1; $i<count($attStmt['x5c']); $i++) {
|
||||
$this->_x5c_chain[] = $attStmt['x5c'][$i]->getBinaryString();
|
||||
}
|
||||
unset ($i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* returns the key certificate in PEM format
|
||||
* @return string
|
||||
*/
|
||||
public function getCertificatePem() {
|
||||
return $this->_createCertificatePem($this->_x5c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $clientDataHash
|
||||
*/
|
||||
public function validateAttestation($clientDataHash) {
|
||||
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||
|
||||
if ($publicKey === false) {
|
||||
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
// Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
|
||||
// using the attestation public key in attestnCert with the algorithm specified in alg.
|
||||
$dataToVerify = $this->_authenticatorData->getBinary();
|
||||
$dataToVerify .= $clientDataHash;
|
||||
|
||||
$coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
|
||||
|
||||
// check certificate
|
||||
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the certificate against root certificates
|
||||
* @param array $rootCas
|
||||
* @return boolean
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function validateRootCertificate($rootCas) {
|
||||
$chainC = $this->_createX5cChainFile();
|
||||
if ($chainC) {
|
||||
$rootCas[] = $chainC;
|
||||
}
|
||||
|
||||
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||
if ($v === -1) {
|
||||
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
}
|
||||
|
||||
141
data/web/inc/lib/WebAuthn/Attestation/Format/AndroidSafetyNet.php
Executable file
141
data/web/inc/lib/WebAuthn/Attestation/Format/AndroidSafetyNet.php
Executable file
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace lbuchs\WebAuthn\Attestation\Format;
|
||||
use lbuchs\WebAuthn\Attestation\AuthenticatorData;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
use lbuchs\WebAuthn\Binary\ByteBuffer;
|
||||
|
||||
class AndroidSafetyNet extends FormatBase {
|
||||
private $_signature;
|
||||
private $_signedValue;
|
||||
private $_x5c;
|
||||
private $_payload;
|
||||
|
||||
public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
|
||||
parent::__construct($AttestionObject, $authenticatorData);
|
||||
|
||||
// check data
|
||||
$attStmt = $this->_attestationObject['attStmt'];
|
||||
|
||||
if (!\array_key_exists('ver', $attStmt) || !$attStmt['ver']) {
|
||||
throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('response', $attStmt) || !($attStmt['response'] instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$response = $attStmt['response']->getBinaryString();
|
||||
|
||||
// Response is a JWS [RFC7515] object in Compact Serialization.
|
||||
// JWSs have three segments separated by two period ('.') characters
|
||||
$parts = \explode('.', $response);
|
||||
unset ($response);
|
||||
if (\count($parts) !== 3) {
|
||||
throw new WebAuthnException('invalid JWS data', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$header = $this->_base64url_decode($parts[0]);
|
||||
$payload = $this->_base64url_decode($parts[1]);
|
||||
$this->_signature = $this->_base64url_decode($parts[2]);
|
||||
$this->_signedValue = $parts[0] . '.' . $parts[1];
|
||||
unset ($parts);
|
||||
|
||||
$header = \json_decode($header);
|
||||
$payload = \json_decode($payload);
|
||||
|
||||
if (!($header instanceof \stdClass)) {
|
||||
throw new WebAuthnException('invalid JWS header', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
if (!($payload instanceof \stdClass)) {
|
||||
throw new WebAuthnException('invalid JWS payload', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!$header->x5c || !is_array($header->x5c) || count($header->x5c) === 0) {
|
||||
throw new WebAuthnException('No X.509 signature in JWS Header', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
// algorithm
|
||||
if (!\in_array($header->alg, array('RS256', 'ES256'))) {
|
||||
throw new WebAuthnException('invalid JWS algorithm ' . $header->alg, WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$this->_x5c = \base64_decode($header->x5c[0]);
|
||||
$this->_payload = $payload;
|
||||
|
||||
if (count($header->x5c) > 1) {
|
||||
for ($i=1; $i<count($header->x5c); $i++) {
|
||||
$this->_x5c_chain[] = \base64_decode($header->x5c[$i]);
|
||||
}
|
||||
unset ($i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* returns the key certificate in PEM format
|
||||
* @return string
|
||||
*/
|
||||
public function getCertificatePem() {
|
||||
return $this->_createCertificatePem($this->_x5c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $clientDataHash
|
||||
*/
|
||||
public function validateAttestation($clientDataHash) {
|
||||
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||
|
||||
// Verify that the nonce in the response is identical to the Base64 encoding
|
||||
// of the SHA-256 hash of the concatenation of authenticatorData and clientDataHash.
|
||||
if (!$this->_payload->nonce || $this->_payload->nonce !== \base64_encode(\hash('SHA256', $this->_authenticatorData->getBinary() . $clientDataHash, true))) {
|
||||
throw new WebAuthnException('invalid nonce in JWS payload', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
// Verify that attestationCert is issued to the hostname "attest.android.com"
|
||||
$certInfo = \openssl_x509_parse($this->getCertificatePem());
|
||||
if (!\is_array($certInfo) || !$certInfo['subject'] || $certInfo['subject']['CN'] !== 'attest.android.com') {
|
||||
throw new WebAuthnException('invalid certificate CN in JWS (' . $certInfo['subject']['CN']. ')', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
// Verify that the ctsProfileMatch attribute in the payload of response is true.
|
||||
if (!$this->_payload->ctsProfileMatch) {
|
||||
throw new WebAuthnException('invalid ctsProfileMatch in payload', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
// check certificate
|
||||
return \openssl_verify($this->_signedValue, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* validates the certificate against root certificates
|
||||
* @param array $rootCas
|
||||
* @return boolean
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function validateRootCertificate($rootCas) {
|
||||
$chainC = $this->_createX5cChainFile();
|
||||
if ($chainC) {
|
||||
$rootCas[] = $chainC;
|
||||
}
|
||||
|
||||
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||
if ($v === -1) {
|
||||
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* decode base64 url
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
private function _base64url_decode($data) {
|
||||
return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4));
|
||||
}
|
||||
}
|
||||
|
||||
139
data/web/inc/lib/WebAuthn/Attestation/Format/Apple.php
Executable file
139
data/web/inc/lib/WebAuthn/Attestation/Format/Apple.php
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace lbuchs\WebAuthn\Attestation\Format;
|
||||
use lbuchs\WebAuthn\Attestation\AuthenticatorData;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
use lbuchs\WebAuthn\Binary\ByteBuffer;
|
||||
|
||||
class Apple extends FormatBase {
|
||||
private $_x5c;
|
||||
|
||||
public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
|
||||
parent::__construct($AttestionObject, $authenticatorData);
|
||||
|
||||
// check packed data
|
||||
$attStmt = $this->_attestationObject['attStmt'];
|
||||
|
||||
|
||||
// certificate for validation
|
||||
if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
|
||||
|
||||
// The attestation certificate attestnCert MUST be the first element in the array
|
||||
$attestnCert = array_shift($attStmt['x5c']);
|
||||
|
||||
if (!($attestnCert instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$this->_x5c = $attestnCert->getBinaryString();
|
||||
|
||||
// certificate chain
|
||||
foreach ($attStmt['x5c'] as $chain) {
|
||||
if ($chain instanceof ByteBuffer) {
|
||||
$this->_x5c_chain[] = $chain->getBinaryString();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new WebAuthnException('invalid Apple attestation statement: missing x5c', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* returns the key certificate in PEM format
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCertificatePem() {
|
||||
return $this->_createCertificatePem($this->_x5c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $clientDataHash
|
||||
*/
|
||||
public function validateAttestation($clientDataHash) {
|
||||
return $this->_validateOverX5c($clientDataHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the certificate against root certificates
|
||||
* @param array $rootCas
|
||||
* @return boolean
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function validateRootCertificate($rootCas) {
|
||||
$chainC = $this->_createX5cChainFile();
|
||||
if ($chainC) {
|
||||
$rootCas[] = $chainC;
|
||||
}
|
||||
|
||||
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||
if ($v === -1) {
|
||||
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
|
||||
/**
|
||||
* validate if x5c is present
|
||||
* @param string $clientDataHash
|
||||
* @return bool
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
protected function _validateOverX5c($clientDataHash) {
|
||||
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||
|
||||
if ($publicKey === false) {
|
||||
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
// Concatenate authenticatorData and clientDataHash to form nonceToHash.
|
||||
$nonceToHash = $this->_authenticatorData->getBinary();
|
||||
$nonceToHash .= $clientDataHash;
|
||||
|
||||
// Perform SHA-256 hash of nonceToHash to produce nonce
|
||||
$nonce = hash('SHA256', $nonceToHash, true);
|
||||
|
||||
$credCert = openssl_x509_read($this->getCertificatePem());
|
||||
if ($credCert === false) {
|
||||
throw new WebAuthnException('invalid x5c certificate: ' . \openssl_error_string(), WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$keyData = openssl_pkey_get_details(openssl_pkey_get_public($credCert));
|
||||
$key = is_array($keyData) && array_key_exists('key', $keyData) ? $keyData['key'] : null;
|
||||
|
||||
|
||||
// Verify that nonce equals the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in credCert.
|
||||
$parsedCredCert = openssl_x509_parse($credCert);
|
||||
$nonceExtension = isset($parsedCredCert['extensions']['1.2.840.113635.100.8.2']) ? $parsedCredCert['extensions']['1.2.840.113635.100.8.2'] : '';
|
||||
|
||||
// nonce padded by ASN.1 string: 30 24 A1 22 04 20
|
||||
// 30 — type tag indicating sequence
|
||||
// 24 — 36 byte following
|
||||
// A1 — Enumerated [1]
|
||||
// 22 — 34 byte following
|
||||
// 04 — type tag indicating octet string
|
||||
// 20 — 32 byte following
|
||||
|
||||
$asn1Padding = "\x30\x24\xA1\x22\x04\x20";
|
||||
if (substr($nonceExtension, 0, strlen($asn1Padding)) === $asn1Padding) {
|
||||
$nonceExtension = substr($nonceExtension, strlen($asn1Padding));
|
||||
}
|
||||
|
||||
if ($nonceExtension !== $nonce) {
|
||||
throw new WebAuthnException('nonce doesn\'t equal the value of the extension with OID 1.2.840.113635.100.8.2', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
// Verify that the credential public key equals the Subject Public Key of credCert.
|
||||
$authKeyData = openssl_pkey_get_details(openssl_pkey_get_public($this->_authenticatorData->getPublicKeyPem()));
|
||||
$authKey = is_array($authKeyData) && array_key_exists('key', $authKeyData) ? $authKeyData['key'] : null;
|
||||
|
||||
if ($key === null || $key !== $authKey) {
|
||||
throw new WebAuthnException('credential public key doesn\'t equal the Subject Public Key of credCert', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
184
data/web/inc/lib/WebAuthn/Attestation/Format/FormatBase.php
Executable file
184
data/web/inc/lib/WebAuthn/Attestation/Format/FormatBase.php
Executable file
|
|
@ -0,0 +1,184 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace lbuchs\WebAuthn\Attestation\Format;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
use lbuchs\WebAuthn\Attestation\AuthenticatorData;
|
||||
|
||||
|
||||
abstract class FormatBase {
|
||||
protected $_attestationObject = null;
|
||||
protected $_authenticatorData = null;
|
||||
protected $_x5c_chain = array();
|
||||
protected $_x5c_tempFile = null;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param Array $AttestionObject
|
||||
* @param AuthenticatorData $authenticatorData
|
||||
*/
|
||||
public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
|
||||
$this->_attestationObject = $AttestionObject;
|
||||
$this->_authenticatorData = $authenticatorData;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __destruct() {
|
||||
// delete X.509 chain certificate file after use
|
||||
if ($this->_x5c_tempFile && \is_file($this->_x5c_tempFile)) {
|
||||
\unlink($this->_x5c_tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the certificate chain in PEM format
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCertificateChain() {
|
||||
if ($this->_x5c_tempFile && \is_file($this->_x5c_tempFile)) {
|
||||
return \file_get_contents($this->_x5c_tempFile);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the key X.509 certificate in PEM format
|
||||
* @return string
|
||||
*/
|
||||
public function getCertificatePem() {
|
||||
// need to be overwritten
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* checks validity of the signature
|
||||
* @param string $clientDataHash
|
||||
* @return bool
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function validateAttestation($clientDataHash) {
|
||||
// need to be overwritten
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the certificate against root certificates
|
||||
* @param array $rootCas
|
||||
* @return boolean
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function validateRootCertificate($rootCas) {
|
||||
// need to be overwritten
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* create a PEM encoded certificate with X.509 binary data
|
||||
* @param string $x5c
|
||||
* @return string
|
||||
*/
|
||||
protected function _createCertificatePem($x5c) {
|
||||
$pem = '-----BEGIN CERTIFICATE-----' . "\n";
|
||||
$pem .= \chunk_split(\base64_encode($x5c), 64, "\n");
|
||||
$pem .= '-----END CERTIFICATE-----' . "\n";
|
||||
return $pem;
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a PEM encoded chain file
|
||||
* @return type
|
||||
*/
|
||||
protected function _createX5cChainFile() {
|
||||
$content = '';
|
||||
if (\is_array($this->_x5c_chain) && \count($this->_x5c_chain) > 0) {
|
||||
foreach ($this->_x5c_chain as $x5c) {
|
||||
$certInfo = \openssl_x509_parse($this->_createCertificatePem($x5c));
|
||||
// check if issuer = subject (self signed)
|
||||
if (\is_array($certInfo) && \is_array($certInfo['issuer']) && \is_array($certInfo['subject'])) {
|
||||
$selfSigned = true;
|
||||
foreach ($certInfo['issuer'] as $k => $v) {
|
||||
if ($certInfo['subject'][$k] !== $v) {
|
||||
$selfSigned = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$selfSigned) {
|
||||
$content .= "\n" . $this->_createCertificatePem($x5c) . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($content) {
|
||||
$this->_x5c_tempFile = \sys_get_temp_dir() . '/x5c_chain_' . \base_convert(\rand(), 10, 36) . '.pem';
|
||||
if (\file_put_contents($this->_x5c_tempFile, $content) !== false) {
|
||||
return $this->_x5c_tempFile;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* returns the name and openssl key for provided cose number.
|
||||
* @param int $coseNumber
|
||||
* @return \stdClass|null
|
||||
*/
|
||||
protected function _getCoseAlgorithm($coseNumber) {
|
||||
// https://www.iana.org/assignments/cose/cose.xhtml#algorithms
|
||||
$coseAlgorithms = array(
|
||||
array(
|
||||
'hash' => 'SHA1',
|
||||
'openssl' => OPENSSL_ALGO_SHA1,
|
||||
'cose' => array(
|
||||
-65535 // RS1
|
||||
)),
|
||||
|
||||
array(
|
||||
'hash' => 'SHA256',
|
||||
'openssl' => OPENSSL_ALGO_SHA256,
|
||||
'cose' => array(
|
||||
-257, // RS256
|
||||
-37, // PS256
|
||||
-7, // ES256
|
||||
5 // HMAC256
|
||||
)),
|
||||
|
||||
array(
|
||||
'hash' => 'SHA384',
|
||||
'openssl' => OPENSSL_ALGO_SHA384,
|
||||
'cose' => array(
|
||||
-258, // RS384
|
||||
-38, // PS384
|
||||
-35, // ES384
|
||||
6 // HMAC384
|
||||
)),
|
||||
|
||||
array(
|
||||
'hash' => 'SHA512',
|
||||
'openssl' => OPENSSL_ALGO_SHA512,
|
||||
'cose' => array(
|
||||
-259, // RS512
|
||||
-39, // PS512
|
||||
-36, // ES512
|
||||
7 // HMAC512
|
||||
))
|
||||
);
|
||||
|
||||
foreach ($coseAlgorithms as $coseAlgorithm) {
|
||||
if (\in_array($coseNumber, $coseAlgorithm['cose'], true)) {
|
||||
$return = new \stdClass();
|
||||
$return->hash = $coseAlgorithm['hash'];
|
||||
$return->openssl = $coseAlgorithm['openssl'];
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
41
data/web/inc/lib/WebAuthn/Attestation/Format/None.php
Executable file
41
data/web/inc/lib/WebAuthn/Attestation/Format/None.php
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace lbuchs\WebAuthn\Attestation\Format;
|
||||
use lbuchs\WebAuthn\Attestation\AuthenticatorData;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
|
||||
class None extends FormatBase {
|
||||
|
||||
|
||||
public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
|
||||
parent::__construct($AttestionObject, $authenticatorData);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* returns the key certificate in PEM format
|
||||
* @return string
|
||||
*/
|
||||
public function getCertificatePem() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $clientDataHash
|
||||
*/
|
||||
public function validateAttestation($clientDataHash) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the certificate against root certificates.
|
||||
* Format 'none' does not contain any ca, so always false.
|
||||
* @param array $rootCas
|
||||
* @return boolean
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function validateRootCertificate($rootCas) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
139
data/web/inc/lib/WebAuthn/Attestation/Format/Packed.php
Executable file
139
data/web/inc/lib/WebAuthn/Attestation/Format/Packed.php
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace lbuchs\WebAuthn\Attestation\Format;
|
||||
use lbuchs\WebAuthn\Attestation\AuthenticatorData;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
use lbuchs\WebAuthn\Binary\ByteBuffer;
|
||||
|
||||
class Packed extends FormatBase {
|
||||
private $_alg;
|
||||
private $_signature;
|
||||
private $_x5c;
|
||||
|
||||
public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
|
||||
parent::__construct($AttestionObject, $authenticatorData);
|
||||
|
||||
// check packed data
|
||||
$attStmt = $this->_attestationObject['attStmt'];
|
||||
|
||||
if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
|
||||
throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$this->_alg = $attStmt['alg'];
|
||||
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||
|
||||
// certificate for validation
|
||||
if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
|
||||
|
||||
// The attestation certificate attestnCert MUST be the first element in the array
|
||||
$attestnCert = array_shift($attStmt['x5c']);
|
||||
|
||||
if (!($attestnCert instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$this->_x5c = $attestnCert->getBinaryString();
|
||||
|
||||
// certificate chain
|
||||
foreach ($attStmt['x5c'] as $chain) {
|
||||
if ($chain instanceof ByteBuffer) {
|
||||
$this->_x5c_chain[] = $chain->getBinaryString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* returns the key certificate in PEM format
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCertificatePem() {
|
||||
if (!$this->_x5c) {
|
||||
return null;
|
||||
}
|
||||
return $this->_createCertificatePem($this->_x5c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $clientDataHash
|
||||
*/
|
||||
public function validateAttestation($clientDataHash) {
|
||||
if ($this->_x5c) {
|
||||
return $this->_validateOverX5c($clientDataHash);
|
||||
} else {
|
||||
return $this->_validateSelfAttestation($clientDataHash);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the certificate against root certificates
|
||||
* @param array $rootCas
|
||||
* @return boolean
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function validateRootCertificate($rootCas) {
|
||||
if (!$this->_x5c) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$chainC = $this->_createX5cChainFile();
|
||||
if ($chainC) {
|
||||
$rootCas[] = $chainC;
|
||||
}
|
||||
|
||||
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||
if ($v === -1) {
|
||||
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
|
||||
/**
|
||||
* validate if x5c is present
|
||||
* @param string $clientDataHash
|
||||
* @return bool
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
protected function _validateOverX5c($clientDataHash) {
|
||||
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||
|
||||
if ($publicKey === false) {
|
||||
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
// Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
|
||||
// using the attestation public key in attestnCert with the algorithm specified in alg.
|
||||
$dataToVerify = $this->_authenticatorData->getBinary();
|
||||
$dataToVerify .= $clientDataHash;
|
||||
|
||||
$coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
|
||||
|
||||
// check certificate
|
||||
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* validate if self attestation is in use
|
||||
* @param string $clientDataHash
|
||||
* @return bool
|
||||
*/
|
||||
protected function _validateSelfAttestation($clientDataHash) {
|
||||
// Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
|
||||
// using the credential public key with alg.
|
||||
$dataToVerify = $this->_authenticatorData->getBinary();
|
||||
$dataToVerify .= $clientDataHash;
|
||||
|
||||
$publicKey = $this->_authenticatorData->getPublicKeyPem();
|
||||
|
||||
// check certificate
|
||||
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1;
|
||||
}
|
||||
}
|
||||
|
||||
180
data/web/inc/lib/WebAuthn/Attestation/Format/Tpm.php
Executable file
180
data/web/inc/lib/WebAuthn/Attestation/Format/Tpm.php
Executable file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace lbuchs\WebAuthn\Attestation\Format;
|
||||
use lbuchs\WebAuthn\Attestation\AuthenticatorData;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
use lbuchs\WebAuthn\Binary\ByteBuffer;
|
||||
|
||||
class Tpm extends FormatBase {
|
||||
private $_TPM_GENERATED_VALUE = "\xFF\x54\x43\x47";
|
||||
private $_TPM_ST_ATTEST_CERTIFY = "\x80\x17";
|
||||
private $_alg;
|
||||
private $_signature;
|
||||
private $_pubArea;
|
||||
private $_x5c;
|
||||
|
||||
/**
|
||||
* @var ByteBuffer
|
||||
*/
|
||||
private $_certInfo;
|
||||
|
||||
|
||||
public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
|
||||
parent::__construct($AttestionObject, $authenticatorData);
|
||||
|
||||
// check packed data
|
||||
$attStmt = $this->_attestationObject['attStmt'];
|
||||
|
||||
if (!\array_key_exists('ver', $attStmt) || $attStmt['ver'] !== '2.0') {
|
||||
throw new WebAuthnException('invalid tpm version: ' . $attStmt['ver'], WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
|
||||
throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('signature not found', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('certInfo', $attStmt) || !\is_object($attStmt['certInfo']) || !($attStmt['certInfo'] instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('certInfo not found', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('pubArea', $attStmt) || !\is_object($attStmt['pubArea']) || !($attStmt['pubArea'] instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('pubArea not found', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$this->_alg = $attStmt['alg'];
|
||||
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||
$this->_certInfo = $attStmt['certInfo'];
|
||||
$this->_pubArea = $attStmt['pubArea'];
|
||||
|
||||
// certificate for validation
|
||||
if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
|
||||
|
||||
// The attestation certificate attestnCert MUST be the first element in the array
|
||||
$attestnCert = array_shift($attStmt['x5c']);
|
||||
|
||||
if (!($attestnCert instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$this->_x5c = $attestnCert->getBinaryString();
|
||||
|
||||
// certificate chain
|
||||
foreach ($attStmt['x5c'] as $chain) {
|
||||
if ($chain instanceof ByteBuffer) {
|
||||
$this->_x5c_chain[] = $chain->getBinaryString();
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new WebAuthnException('no x5c certificate found', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* returns the key certificate in PEM format
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCertificatePem() {
|
||||
if (!$this->_x5c) {
|
||||
return null;
|
||||
}
|
||||
return $this->_createCertificatePem($this->_x5c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $clientDataHash
|
||||
*/
|
||||
public function validateAttestation($clientDataHash) {
|
||||
return $this->_validateOverX5c($clientDataHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the certificate against root certificates
|
||||
* @param array $rootCas
|
||||
* @return boolean
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function validateRootCertificate($rootCas) {
|
||||
if (!$this->_x5c) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$chainC = $this->_createX5cChainFile();
|
||||
if ($chainC) {
|
||||
$rootCas[] = $chainC;
|
||||
}
|
||||
|
||||
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||
if ($v === -1) {
|
||||
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
|
||||
/**
|
||||
* validate if x5c is present
|
||||
* @param string $clientDataHash
|
||||
* @return bool
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
protected function _validateOverX5c($clientDataHash) {
|
||||
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||
|
||||
if ($publicKey === false) {
|
||||
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
// Concatenate authenticatorData and clientDataHash to form attToBeSigned.
|
||||
$attToBeSigned = $this->_authenticatorData->getBinary();
|
||||
$attToBeSigned .= $clientDataHash;
|
||||
|
||||
// Validate that certInfo is valid:
|
||||
|
||||
// Verify that magic is set to TPM_GENERATED_VALUE.
|
||||
if ($this->_certInfo->getBytes(0, 4) !== $this->_TPM_GENERATED_VALUE) {
|
||||
throw new WebAuthnException('tpm magic not TPM_GENERATED_VALUE', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
// Verify that type is set to TPM_ST_ATTEST_CERTIFY.
|
||||
if ($this->_certInfo->getBytes(4, 2) !== $this->_TPM_ST_ATTEST_CERTIFY) {
|
||||
throw new WebAuthnException('tpm type not TPM_ST_ATTEST_CERTIFY', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$offset = 6;
|
||||
$qualifiedSigner = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset);
|
||||
$extraData = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset);
|
||||
$coseAlg = $this->_getCoseAlgorithm($this->_alg);
|
||||
|
||||
// Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
|
||||
if ($extraData->getBinaryString() !== \hash($coseAlg->hash, $attToBeSigned, true)) {
|
||||
throw new WebAuthnException('certInfo:extraData not hash of attToBeSigned', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
// Verify the sig is a valid signature over certInfo using the attestation
|
||||
// public key in aikCert with the algorithm specified in alg.
|
||||
return \openssl_verify($this->_certInfo->getBinaryString(), $this->_signature, $publicKey, $coseAlg->openssl) === 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* returns next part of ByteBuffer
|
||||
* @param ByteBuffer $buffer
|
||||
* @param int $offset
|
||||
* @return ByteBuffer
|
||||
*/
|
||||
protected function _tpmReadLengthPrefixed(ByteBuffer $buffer, &$offset) {
|
||||
$len = $buffer->getUint16Val($offset);
|
||||
$data = $buffer->getBytes($offset + 2, $len);
|
||||
$offset += (2 + $len);
|
||||
|
||||
return new ByteBuffer($data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
93
data/web/inc/lib/WebAuthn/Attestation/Format/U2f.php
Executable file
93
data/web/inc/lib/WebAuthn/Attestation/Format/U2f.php
Executable file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace lbuchs\WebAuthn\Attestation\Format;
|
||||
use lbuchs\WebAuthn\Attestation\AuthenticatorData;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
use lbuchs\WebAuthn\Binary\ByteBuffer;
|
||||
|
||||
class U2f extends FormatBase {
|
||||
private $_alg = -7;
|
||||
private $_signature;
|
||||
private $_x5c;
|
||||
|
||||
public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
|
||||
parent::__construct($AttestionObject, $authenticatorData);
|
||||
|
||||
// check u2f data
|
||||
$attStmt = $this->_attestationObject['attStmt'];
|
||||
|
||||
if (\array_key_exists('alg', $attStmt) && $attStmt['alg'] !== $this->_alg) {
|
||||
throw new WebAuthnException('u2f only accepts algorithm -7 ("ES256"), but got ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) !== 1) {
|
||||
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) {
|
||||
throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
$this->_signature = $attStmt['sig']->getBinaryString();
|
||||
$this->_x5c = $attStmt['x5c'][0]->getBinaryString();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* returns the key certificate in PEM format
|
||||
* @return string
|
||||
*/
|
||||
public function getCertificatePem() {
|
||||
$pem = '-----BEGIN CERTIFICATE-----' . "\n";
|
||||
$pem .= \chunk_split(\base64_encode($this->_x5c), 64, "\n");
|
||||
$pem .= '-----END CERTIFICATE-----' . "\n";
|
||||
return $pem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $clientDataHash
|
||||
*/
|
||||
public function validateAttestation($clientDataHash) {
|
||||
$publicKey = \openssl_pkey_get_public($this->getCertificatePem());
|
||||
|
||||
if ($publicKey === false) {
|
||||
throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
// Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
|
||||
$dataToVerify = "\x00";
|
||||
$dataToVerify .= $this->_authenticatorData->getRpIdHash();
|
||||
$dataToVerify .= $clientDataHash;
|
||||
$dataToVerify .= $this->_authenticatorData->getCredentialId();
|
||||
$dataToVerify .= $this->_authenticatorData->getPublicKeyU2F();
|
||||
|
||||
$coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
|
||||
|
||||
// check certificate
|
||||
return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the certificate against root certificates
|
||||
* @param array $rootCas
|
||||
* @return boolean
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function validateRootCertificate($rootCas) {
|
||||
$chainC = $this->_createX5cChainFile();
|
||||
if ($chainC) {
|
||||
$rootCas[] = $chainC;
|
||||
}
|
||||
|
||||
$v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
|
||||
if ($v === -1) {
|
||||
throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
}
|
||||
293
data/web/inc/lib/WebAuthn/Binary/ByteBuffer.php
Executable file
293
data/web/inc/lib/WebAuthn/Binary/ByteBuffer.php
Executable file
|
|
@ -0,0 +1,293 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace lbuchs\WebAuthn\Binary;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
|
||||
/**
|
||||
* Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/ByteBuffer.php
|
||||
* Copyright © 2018 Thomas Bleeker - MIT licensed
|
||||
* Modified by Lukas Buchs
|
||||
* Thanks Thomas for your work!
|
||||
*/
|
||||
class ByteBuffer implements \JsonSerializable, \Serializable {
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public static $useBase64UrlEncoding = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $_data;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $_length;
|
||||
|
||||
public function __construct($binaryData) {
|
||||
$this->_data = $binaryData;
|
||||
$this->_length = \strlen($binaryData);
|
||||
}
|
||||
|
||||
|
||||
// -----------------------
|
||||
// PUBLIC STATIC
|
||||
// -----------------------
|
||||
|
||||
/**
|
||||
* create a ByteBuffer from a base64 url encoded string
|
||||
* @param string $base64url
|
||||
* @return ByteBuffer
|
||||
*/
|
||||
public static function fromBase64Url($base64url) {
|
||||
$bin = self::_base64url_decode($base64url);
|
||||
if ($bin === false) {
|
||||
throw new WebAuthnException('ByteBuffer: Invalid base64 url string', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
return new ByteBuffer($bin);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a ByteBuffer from a base64 url encoded string
|
||||
* @param string $hex
|
||||
* @return ByteBuffer
|
||||
*/
|
||||
public static function fromHex($hex) {
|
||||
$bin = \hex2bin($hex);
|
||||
if ($bin === false) {
|
||||
throw new WebAuthnException('ByteBuffer: Invalid hex string', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
return new ByteBuffer($bin);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a random ByteBuffer
|
||||
* @param string $length
|
||||
* @return ByteBuffer
|
||||
*/
|
||||
public static function randomBuffer($length) {
|
||||
if (\function_exists('random_bytes')) { // >PHP 7.0
|
||||
return new ByteBuffer(\random_bytes($length));
|
||||
|
||||
} else if (\function_exists('openssl_random_pseudo_bytes')) {
|
||||
return new ByteBuffer(\openssl_random_pseudo_bytes($length));
|
||||
|
||||
} else {
|
||||
throw new WebAuthnException('ByteBuffer: cannot generate random bytes', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// PUBLIC
|
||||
// -----------------------
|
||||
|
||||
public function getBytes($offset, $length) {
|
||||
if ($offset < 0 || $length < 0 || ($offset + $length > $this->_length)) {
|
||||
throw new WebAuthnException('ByteBuffer: Invalid offset or length', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
return \substr($this->_data, $offset, $length);
|
||||
}
|
||||
|
||||
public function getByteVal($offset) {
|
||||
if ($offset < 0 || $offset >= $this->_length) {
|
||||
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
return \ord(\substr($this->_data, $offset, 1));
|
||||
}
|
||||
|
||||
public function getJson($jsonFlags=0) {
|
||||
$data = \json_decode($this->getBinaryString(), null, 512, $jsonFlags);
|
||||
if (\json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new WebAuthnException(\json_last_error_msg(), WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getLength() {
|
||||
return $this->_length;
|
||||
}
|
||||
|
||||
public function getUint16Val($offset) {
|
||||
if ($offset < 0 || ($offset + 2) > $this->_length) {
|
||||
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
return unpack('n', $this->_data, $offset)[1];
|
||||
}
|
||||
|
||||
public function getUint32Val($offset) {
|
||||
if ($offset < 0 || ($offset + 4) > $this->_length) {
|
||||
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
$val = unpack('N', $this->_data, $offset)[1];
|
||||
|
||||
// Signed integer overflow causes signed negative numbers
|
||||
if ($val < 0) {
|
||||
throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function getUint64Val($offset) {
|
||||
if (PHP_INT_SIZE < 8) {
|
||||
throw new WebAuthnException('ByteBuffer: 64-bit values not supported by this system', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
if ($offset < 0 || ($offset + 8) > $this->_length) {
|
||||
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
$val = unpack('J', $this->_data, $offset)[1];
|
||||
|
||||
// Signed integer overflow causes signed negative numbers
|
||||
if ($val < 0) {
|
||||
throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function getHalfFloatVal($offset) {
|
||||
//FROM spec pseudo decode_half(unsigned char *halfp)
|
||||
$half = $this->getUint16Val($offset);
|
||||
|
||||
$exp = ($half >> 10) & 0x1f;
|
||||
$mant = $half & 0x3ff;
|
||||
|
||||
if ($exp === 0) {
|
||||
$val = $mant * (2 ** -24);
|
||||
} elseif ($exp !== 31) {
|
||||
$val = ($mant + 1024) * (2 ** ($exp - 25));
|
||||
} else {
|
||||
$val = ($mant === 0) ? INF : NAN;
|
||||
}
|
||||
|
||||
return ($half & 0x8000) ? -$val : $val;
|
||||
}
|
||||
|
||||
public function getFloatVal($offset) {
|
||||
if ($offset < 0 || ($offset + 4) > $this->_length) {
|
||||
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
return unpack('G', $this->_data, $offset)[1];
|
||||
}
|
||||
|
||||
public function getDoubleVal($offset) {
|
||||
if ($offset < 0 || ($offset + 8) > $this->_length) {
|
||||
throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
|
||||
}
|
||||
return unpack('E', $this->_data, $offset)[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBinaryString() {
|
||||
return $this->_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $buffer
|
||||
* @return bool
|
||||
*/
|
||||
public function equals($buffer) {
|
||||
return is_string($this->_data) && $this->_data === $buffer->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHex() {
|
||||
return \bin2hex($this->_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmpty() {
|
||||
return $this->_length === 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* jsonSerialize interface
|
||||
* return binary data in RFC 1342-Like serialized string
|
||||
* @return string
|
||||
*/
|
||||
public function jsonSerialize() {
|
||||
if (ByteBuffer::$useBase64UrlEncoding) {
|
||||
return self::_base64url_encode($this->_data);
|
||||
|
||||
} else {
|
||||
return '=?BINARY?B?' . \base64_encode($this->_data) . '?=';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializable-Interface
|
||||
* @return string
|
||||
*/
|
||||
public function serialize() {
|
||||
return \serialize($this->_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializable-Interface
|
||||
* @param string $serialized
|
||||
*/
|
||||
public function unserialize($serialized) {
|
||||
$this->_data = \unserialize($serialized);
|
||||
$this->_length = \strlen($this->_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 8 deprecates Serializable-Interface)
|
||||
* @return array
|
||||
*/
|
||||
public function __serialize() {
|
||||
return [
|
||||
'data' => \serialize($this->_data)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* object to string
|
||||
* @return string
|
||||
*/
|
||||
public function __toString() {
|
||||
return $this->getHex();
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 8 deprecates Serializable-Interface)
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function __unserialize($data) {
|
||||
if ($data && isset($data['data'])) {
|
||||
$this->_data = \unserialize($data['data']);
|
||||
$this->_length = \strlen($this->_data);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// PROTECTED STATIC
|
||||
// -----------------------
|
||||
|
||||
/**
|
||||
* base64 url decoding
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
protected static function _base64url_decode($data) {
|
||||
return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4));
|
||||
}
|
||||
|
||||
/**
|
||||
* base64 url encoding
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
protected static function _base64url_encode($data) {
|
||||
return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
}
|
||||
220
data/web/inc/lib/WebAuthn/CBOR/CborDecoder.php
Executable file
220
data/web/inc/lib/WebAuthn/CBOR/CborDecoder.php
Executable file
|
|
@ -0,0 +1,220 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace lbuchs\WebAuthn\CBOR;
|
||||
use lbuchs\WebAuthn\WebAuthnException;
|
||||
use lbuchs\WebAuthn\Binary\ByteBuffer;
|
||||
|
||||
/**
|
||||
* Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/CborDecoder.php
|
||||
* Copyright © 2018 Thomas Bleeker - MIT licensed
|
||||
* Modified by Lukas Buchs
|
||||
* Thanks Thomas for your work!
|
||||
*/
|
||||
class CborDecoder {
|
||||
const CBOR_MAJOR_UNSIGNED_INT = 0;
|
||||
const CBOR_MAJOR_TEXT_STRING = 3;
|
||||
const CBOR_MAJOR_FLOAT_SIMPLE = 7;
|
||||
const CBOR_MAJOR_NEGATIVE_INT = 1;
|
||||
const CBOR_MAJOR_ARRAY = 4;
|
||||
const CBOR_MAJOR_TAG = 6;
|
||||
const CBOR_MAJOR_MAP = 5;
|
||||
const CBOR_MAJOR_BYTE_STRING = 2;
|
||||
|
||||
/**
|
||||
* @param ByteBuffer|string $bufOrBin
|
||||
* @return mixed
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public static function decode($bufOrBin) {
|
||||
$buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin);
|
||||
|
||||
$offset = 0;
|
||||
$result = self::_parseItem($buf, $offset);
|
||||
if ($offset !== $buf->getLength()) {
|
||||
throw new WebAuthnException('Unused bytes after data item.', WebAuthnException::CBOR);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ByteBuffer|string $bufOrBin
|
||||
* @param int $startOffset
|
||||
* @param int|null $endOffset
|
||||
* @return mixed
|
||||
*/
|
||||
public static function decodeInPlace($bufOrBin, $startOffset, &$endOffset = null) {
|
||||
$buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin);
|
||||
|
||||
$offset = $startOffset;
|
||||
$data = self::_parseItem($buf, $offset);
|
||||
$endOffset = $offset;
|
||||
return $data;
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// protected
|
||||
// ---------------------
|
||||
|
||||
/**
|
||||
* @param ByteBuffer $buf
|
||||
* @param int $offset
|
||||
* @return mixed
|
||||
*/
|
||||
protected static function _parseItem(ByteBuffer $buf, &$offset) {
|
||||
$first = $buf->getByteVal($offset++);
|
||||
$type = $first >> 5;
|
||||
$val = $first & 0b11111;
|
||||
|
||||
if ($type === self::CBOR_MAJOR_FLOAT_SIMPLE) {
|
||||
return self::_parseFloatSimple($val, $buf, $offset);
|
||||
}
|
||||
|
||||
$val = self::_parseExtraLength($val, $buf, $offset);
|
||||
|
||||
return self::_parseItemData($type, $val, $buf, $offset);
|
||||
}
|
||||
|
||||
protected static function _parseFloatSimple($val, ByteBuffer $buf, &$offset) {
|
||||
switch ($val) {
|
||||
case 24:
|
||||
$val = $buf->getByteVal($offset);
|
||||
$offset++;
|
||||
return self::_parseSimple($val);
|
||||
|
||||
case 25:
|
||||
$floatValue = $buf->getHalfFloatVal($offset);
|
||||
$offset += 2;
|
||||
return $floatValue;
|
||||
|
||||
case 26:
|
||||
$floatValue = $buf->getFloatVal($offset);
|
||||
$offset += 4;
|
||||
return $floatValue;
|
||||
|
||||
case 27:
|
||||
$floatValue = $buf->getDoubleVal($offset);
|
||||
$offset += 8;
|
||||
return $floatValue;
|
||||
|
||||
case 28:
|
||||
case 29:
|
||||
case 30:
|
||||
throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR);
|
||||
|
||||
case 31:
|
||||
throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR);
|
||||
}
|
||||
|
||||
return self::_parseSimple($val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $val
|
||||
* @return mixed
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
protected static function _parseSimple($val) {
|
||||
if ($val === 20) {
|
||||
return false;
|
||||
}
|
||||
if ($val === 21) {
|
||||
return true;
|
||||
}
|
||||
if ($val === 22) {
|
||||
return null;
|
||||
}
|
||||
throw new WebAuthnException(sprintf('Unsupported simple value %d.', $val), WebAuthnException::CBOR);
|
||||
}
|
||||
|
||||
protected static function _parseExtraLength($val, ByteBuffer $buf, &$offset) {
|
||||
switch ($val) {
|
||||
case 24:
|
||||
$val = $buf->getByteVal($offset);
|
||||
$offset++;
|
||||
break;
|
||||
|
||||
case 25:
|
||||
$val = $buf->getUint16Val($offset);
|
||||
$offset += 2;
|
||||
break;
|
||||
|
||||
case 26:
|
||||
$val = $buf->getUint32Val($offset);
|
||||
$offset += 4;
|
||||
break;
|
||||
|
||||
case 27:
|
||||
$val = $buf->getUint64Val($offset);
|
||||
$offset += 8;
|
||||
break;
|
||||
|
||||
case 28:
|
||||
case 29:
|
||||
case 30:
|
||||
throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR);
|
||||
|
||||
case 31:
|
||||
throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR);
|
||||
}
|
||||
|
||||
return $val;
|
||||
}
|
||||
|
||||
protected static function _parseItemData($type, $val, ByteBuffer $buf, &$offset) {
|
||||
switch ($type) {
|
||||
case self::CBOR_MAJOR_UNSIGNED_INT: // uint
|
||||
return $val;
|
||||
|
||||
case self::CBOR_MAJOR_NEGATIVE_INT:
|
||||
return -1 - $val;
|
||||
|
||||
case self::CBOR_MAJOR_BYTE_STRING:
|
||||
$data = $buf->getBytes($offset, $val);
|
||||
$offset += $val;
|
||||
return new ByteBuffer($data); // bytes
|
||||
|
||||
case self::CBOR_MAJOR_TEXT_STRING:
|
||||
$data = $buf->getBytes($offset, $val);
|
||||
$offset += $val;
|
||||
return $data; // UTF-8
|
||||
|
||||
case self::CBOR_MAJOR_ARRAY:
|
||||
return self::_parseArray($buf, $offset, $val);
|
||||
|
||||
case self::CBOR_MAJOR_MAP:
|
||||
return self::_parseMap($buf, $offset, $val);
|
||||
|
||||
case self::CBOR_MAJOR_TAG:
|
||||
return self::_parseItem($buf, $offset); // 1 embedded data item
|
||||
}
|
||||
|
||||
// This should never be reached
|
||||
throw new WebAuthnException(sprintf('Unknown major type %d.', $type), WebAuthnException::CBOR);
|
||||
}
|
||||
|
||||
protected static function _parseMap(ByteBuffer $buf, &$offset, $count) {
|
||||
$map = array();
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$mapKey = self::_parseItem($buf, $offset);
|
||||
$mapVal = self::_parseItem($buf, $offset);
|
||||
|
||||
if (!\is_int($mapKey) && !\is_string($mapKey)) {
|
||||
throw new WebAuthnException('Can only use strings or integers as map keys', WebAuthnException::CBOR);
|
||||
}
|
||||
|
||||
$map[$mapKey] = $mapVal; // todo dup
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
protected static function _parseArray(ByteBuffer $buf, &$offset, $count) {
|
||||
$arr = array();
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$arr[] = self::_parseItem($buf, $offset);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
}
|
||||
593
data/web/inc/lib/WebAuthn/WebAuthn.php
Executable file
593
data/web/inc/lib/WebAuthn/WebAuthn.php
Executable file
|
|
@ -0,0 +1,593 @@
|
|||
<?php
|
||||
|
||||
namespace lbuchs\WebAuthn;
|
||||
use lbuchs\WebAuthn\Binary\ByteBuffer;
|
||||
require_once 'WebAuthnException.php';
|
||||
require_once 'Binary/ByteBuffer.php';
|
||||
require_once 'Attestation/AttestationObject.php';
|
||||
require_once 'Attestation/AuthenticatorData.php';
|
||||
require_once 'Attestation/Format/FormatBase.php';
|
||||
require_once 'Attestation/Format/None.php';
|
||||
require_once 'Attestation/Format/AndroidKey.php';
|
||||
require_once 'Attestation/Format/AndroidSafetyNet.php';
|
||||
require_once 'Attestation/Format/Apple.php';
|
||||
require_once 'Attestation/Format/Packed.php';
|
||||
require_once 'Attestation/Format/Tpm.php';
|
||||
require_once 'Attestation/Format/U2f.php';
|
||||
require_once 'CBOR/CborDecoder.php';
|
||||
|
||||
/**
|
||||
* WebAuthn
|
||||
* @author Lukas Buchs
|
||||
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||
*/
|
||||
class WebAuthn {
|
||||
// relying party
|
||||
private $_rpName;
|
||||
private $_rpId;
|
||||
private $_rpIdHash;
|
||||
private $_challenge;
|
||||
private $_signatureCounter;
|
||||
private $_caFiles;
|
||||
private $_formats;
|
||||
|
||||
/**
|
||||
* Initialize a new WebAuthn server
|
||||
* @param string $rpName the relying party name
|
||||
* @param string $rpId the relying party ID = the domain name
|
||||
* @param bool $useBase64UrlEncoding true to use base64 url encoding for binary data in json objects. Default is a RFC 1342-Like serialized string.
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function __construct($rpName, $rpId, $allowedFormats=null, $useBase64UrlEncoding=false) {
|
||||
$this->_rpName = $rpName;
|
||||
$this->_rpId = $rpId;
|
||||
$this->_rpIdHash = \hash('sha256', $rpId, true);
|
||||
ByteBuffer::$useBase64UrlEncoding = !!$useBase64UrlEncoding;
|
||||
$supportedFormats = array('android-key', 'android-safetynet', 'apple', 'fido-u2f', 'none', 'packed', 'tpm');
|
||||
|
||||
if (!\function_exists('\openssl_open')) {
|
||||
throw new WebAuthnException('OpenSSL-Module not installed');;
|
||||
}
|
||||
|
||||
if (!\in_array('SHA256', \array_map('\strtoupper', \openssl_get_md_methods()))) {
|
||||
throw new WebAuthnException('SHA256 not supported by this openssl installation.');
|
||||
}
|
||||
|
||||
// default: all format
|
||||
if (!is_array($allowedFormats)) {
|
||||
$allowedFormats = $supportedFormats;
|
||||
}
|
||||
$this->_formats = $allowedFormats;
|
||||
|
||||
// validate formats
|
||||
$invalidFormats = \array_diff($this->_formats, $supportedFormats);
|
||||
if (!$this->_formats || $invalidFormats) {
|
||||
throw new WebAuthnException('invalid formats on construct: ' . implode(', ', $invalidFormats));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* add a root certificate to verify new registrations
|
||||
* @param string $path file path of / directory with root certificates
|
||||
* @param array|null $certFileExtensions if adding a direction, all files with provided extension are added. default: pem, crt, cer, der
|
||||
*/
|
||||
public function addRootCertificates($path, $certFileExtensions=null) {
|
||||
if (!\is_array($this->_caFiles)) {
|
||||
$this->_caFiles = array();
|
||||
}
|
||||
if ($certFileExtensions === null) {
|
||||
$certFileExtensions = array('pem', 'crt', 'cer', 'der');
|
||||
}
|
||||
$path = \rtrim(\trim($path), '\\/');
|
||||
if (\is_dir($path)) {
|
||||
foreach (\scandir($path) as $ca) {
|
||||
if (\is_file($path . DIRECTORY_SEPARATOR . $ca) && \in_array(\strtolower(\pathinfo($ca, PATHINFO_EXTENSION)), $certFileExtensions)) {
|
||||
$this->addRootCertificates($path . DIRECTORY_SEPARATOR . $ca);
|
||||
}
|
||||
}
|
||||
} else if (\is_file($path) && !\in_array(\realpath($path), $this->_caFiles)) {
|
||||
$this->_caFiles[] = \realpath($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the generated challenge to save for later validation
|
||||
* @return ByteBuffer
|
||||
*/
|
||||
public function getChallenge() {
|
||||
return $this->_challenge;
|
||||
}
|
||||
|
||||
/**
|
||||
* generates the object for a key registration
|
||||
* provide this data to navigator.credentials.create
|
||||
* @param string $userId
|
||||
* @param string $userName
|
||||
* @param string $userDisplayName
|
||||
* @param int $timeout timeout in seconds
|
||||
* @param bool $requireResidentKey true, if the key should be stored by the authentication device
|
||||
* @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation
|
||||
* if the response does not have the UV flag set.
|
||||
* Valid values:
|
||||
* true = required
|
||||
* false = preferred
|
||||
* string 'required' 'preferred' 'discouraged'
|
||||
* @param bool|null $crossPlatformAttachment true for cross-platform devices (eg. fido usb),
|
||||
* false for platform devices (eg. windows hello, android safetynet),
|
||||
* null for both
|
||||
* @param array $excludeCredentialIds a array of ids, which are already registered, to prevent re-registration
|
||||
* @return \stdClass
|
||||
*/
|
||||
public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20, $requireResidentKey=false, $requireUserVerification=false, $crossPlatformAttachment=null, $excludeCredentialIds=array()) {
|
||||
|
||||
// validate User Verification Requirement
|
||||
if (\is_bool($requireUserVerification)) {
|
||||
$requireUserVerification = $requireUserVerification ? 'required' : 'preferred';
|
||||
} else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) {
|
||||
$requireUserVerification = \strtolower($requireUserVerification);
|
||||
} else {
|
||||
$requireUserVerification = 'preferred';
|
||||
}
|
||||
|
||||
$args = new \stdClass();
|
||||
$args->publicKey = new \stdClass();
|
||||
|
||||
// relying party
|
||||
$args->publicKey->rp = new \stdClass();
|
||||
$args->publicKey->rp->name = $this->_rpName;
|
||||
$args->publicKey->rp->id = $this->_rpId;
|
||||
|
||||
$args->publicKey->authenticatorSelection = new \stdClass();
|
||||
$args->publicKey->authenticatorSelection->userVerification = $requireUserVerification;
|
||||
if ($requireResidentKey) {
|
||||
$args->publicKey->authenticatorSelection->requireResidentKey = true;
|
||||
}
|
||||
if (is_bool($crossPlatformAttachment)) {
|
||||
$args->publicKey->authenticatorSelection->authenticatorAttachment = $crossPlatformAttachment ? 'cross-platform' : 'platform';
|
||||
}
|
||||
|
||||
// user
|
||||
$args->publicKey->user = new \stdClass();
|
||||
$args->publicKey->user->id = new ByteBuffer($userId); // binary
|
||||
$args->publicKey->user->name = $userName;
|
||||
$args->publicKey->user->displayName = $userDisplayName;
|
||||
|
||||
$args->publicKey->pubKeyCredParams = array();
|
||||
$tmp = new \stdClass();
|
||||
$tmp->type = 'public-key';
|
||||
$tmp->alg = -7; // ES256
|
||||
$args->publicKey->pubKeyCredParams[] = $tmp;
|
||||
unset ($tmp);
|
||||
|
||||
$tmp = new \stdClass();
|
||||
$tmp->type = 'public-key';
|
||||
$tmp->alg = -257; // RS256
|
||||
$args->publicKey->pubKeyCredParams[] = $tmp;
|
||||
unset ($tmp);
|
||||
|
||||
// if there are root certificates added, we need direct attestation to validate
|
||||
// against the root certificate. If there are no root-certificates added,
|
||||
// anonymization ca are also accepted, because we can't validate the root anyway.
|
||||
$attestation = 'indirect';
|
||||
if (\is_array($this->_caFiles)) {
|
||||
$attestation = 'direct';
|
||||
}
|
||||
|
||||
$args->publicKey->attestation = \count($this->_formats) === 1 && \in_array('none', $this->_formats) ? 'none' : $attestation;
|
||||
$args->publicKey->extensions = new \stdClass();
|
||||
$args->publicKey->extensions->exts = true;
|
||||
$args->publicKey->timeout = $timeout * 1000; // microseconds
|
||||
$args->publicKey->challenge = $this->_createChallenge(); // binary
|
||||
|
||||
//prevent re-registration by specifying existing credentials
|
||||
$args->publicKey->excludeCredentials = array();
|
||||
|
||||
if (is_array($excludeCredentialIds)) {
|
||||
foreach ($excludeCredentialIds as $id) {
|
||||
$tmp = new \stdClass();
|
||||
$tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary
|
||||
$tmp->type = 'public-key';
|
||||
$tmp->transports = array('usb', 'ble', 'nfc', 'internal');
|
||||
$args->publicKey->excludeCredentials[] = $tmp;
|
||||
unset ($tmp);
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* generates the object for key validation
|
||||
* Provide this data to navigator.credentials.get
|
||||
* @param array $credentialIds binary
|
||||
* @param int $timeout timeout in seconds
|
||||
* @param bool $allowUsb allow removable USB
|
||||
* @param bool $allowNfc allow Near Field Communication (NFC)
|
||||
* @param bool $allowBle allow Bluetooth
|
||||
* @param bool $allowInternal allow client device-specific transport. These authenticators are not removable from the client device.
|
||||
* @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation
|
||||
* if the response does not have the UV flag set.
|
||||
* Valid values:
|
||||
* true = required
|
||||
* false = preferred
|
||||
* string 'required' 'preferred' 'discouraged'
|
||||
* @return \stdClass
|
||||
*/
|
||||
public function getGetArgs($credentialIds=array(), $timeout=20, $allowUsb=true, $allowNfc=true, $allowBle=true, $allowInternal=true, $requireUserVerification=false) {
|
||||
|
||||
// validate User Verification Requirement
|
||||
if (\is_bool($requireUserVerification)) {
|
||||
$requireUserVerification = $requireUserVerification ? 'required' : 'preferred';
|
||||
} else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) {
|
||||
$requireUserVerification = \strtolower($requireUserVerification);
|
||||
} else {
|
||||
$requireUserVerification = 'preferred';
|
||||
}
|
||||
|
||||
$args = new \stdClass();
|
||||
$args->publicKey = new \stdClass();
|
||||
$args->publicKey->timeout = $timeout * 1000; // microseconds
|
||||
$args->publicKey->challenge = $this->_createChallenge(); // binary
|
||||
$args->publicKey->userVerification = $requireUserVerification;
|
||||
$args->publicKey->rpId = $this->_rpId;
|
||||
|
||||
if (\is_array($credentialIds) && \count($credentialIds) > 0) {
|
||||
$args->publicKey->allowCredentials = array();
|
||||
|
||||
foreach ($credentialIds as $id) {
|
||||
$tmp = new \stdClass();
|
||||
$tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id); // binary
|
||||
$tmp->transports = array();
|
||||
|
||||
if ($allowUsb) {
|
||||
$tmp->transports[] = 'usb';
|
||||
}
|
||||
if ($allowNfc) {
|
||||
$tmp->transports[] = 'nfc';
|
||||
}
|
||||
if ($allowBle) {
|
||||
$tmp->transports[] = 'ble';
|
||||
}
|
||||
if ($allowInternal) {
|
||||
$tmp->transports[] = 'internal';
|
||||
}
|
||||
|
||||
$tmp->type = 'public-key';
|
||||
$args->publicKey->allowCredentials[] = $tmp;
|
||||
unset ($tmp);
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the new signature counter value.
|
||||
* returns null if there is no counter
|
||||
* @return ?int
|
||||
*/
|
||||
public function getSignatureCounter() {
|
||||
return \is_int($this->_signatureCounter) ? $this->_signatureCounter : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* process a create request and returns data to save for future logins
|
||||
* @param string $clientDataJSON binary from browser
|
||||
* @param string $attestationObject binary from browser
|
||||
* @param string|ByteBuffer $challenge binary used challange
|
||||
* @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin)
|
||||
* @param bool $requireUserPresent false, if the device must NOT check user presence (e.g. by pressing a button)
|
||||
* @param bool $failIfRootMismatch false, if there should be no error thrown if root certificate doesn't match
|
||||
* @return \stdClass
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true) {
|
||||
$clientDataHash = \hash('sha256', $clientDataJSON, true);
|
||||
$clientData = \json_decode($clientDataJSON);
|
||||
$challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge);
|
||||
|
||||
// security: https://www.w3.org/TR/webauthn/#registering-a-new-credential
|
||||
|
||||
// 2. Let C, the client data claimed as collected during the credential creation,
|
||||
// be the result of running an implementation-specific JSON parser on JSONtext.
|
||||
if (!\is_object($clientData)) {
|
||||
throw new WebAuthnException('invalid client data', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
// 3. Verify that the value of C.type is webauthn.create.
|
||||
if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.create') {
|
||||
throw new WebAuthnException('invalid type', WebAuthnException::INVALID_TYPE);
|
||||
}
|
||||
|
||||
// 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call.
|
||||
if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
|
||||
throw new WebAuthnException('invalid challenge', WebAuthnException::INVALID_CHALLENGE);
|
||||
}
|
||||
|
||||
// 5. Verify that the value of C.origin matches the Relying Party's origin.
|
||||
if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
|
||||
throw new WebAuthnException('invalid origin', WebAuthnException::INVALID_ORIGIN);
|
||||
}
|
||||
|
||||
// Attestation
|
||||
$attestationObject = new Attestation\AttestationObject($attestationObject, $this->_formats);
|
||||
|
||||
// 9. Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP.
|
||||
if (!$attestationObject->validateRpIdHash($this->_rpIdHash)) {
|
||||
throw new WebAuthnException('invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
|
||||
}
|
||||
|
||||
// 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature
|
||||
if (!$attestationObject->validateAttestation($clientDataHash)) {
|
||||
throw new WebAuthnException('invalid certificate signature', WebAuthnException::INVALID_SIGNATURE);
|
||||
}
|
||||
|
||||
// 15. If validation is successful, obtain a list of acceptable trust anchors
|
||||
$rootValid = is_array($this->_caFiles) ? $attestationObject->validateRootCertificate($this->_caFiles) : null;
|
||||
if ($failIfRootMismatch && is_array($this->_caFiles) && !$rootValid) {
|
||||
throw new WebAuthnException('invalid root certificate', WebAuthnException::CERTIFICATE_NOT_TRUSTED);
|
||||
}
|
||||
|
||||
// 10. Verify that the User Present bit of the flags in authData is set.
|
||||
$userPresent = $attestationObject->getAuthenticatorData()->getUserPresent();
|
||||
if ($requireUserPresent && !$userPresent) {
|
||||
throw new WebAuthnException('user not present during authentication', WebAuthnException::USER_PRESENT);
|
||||
}
|
||||
|
||||
// 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.
|
||||
$userVerified = $attestationObject->getAuthenticatorData()->getUserVerified();
|
||||
if ($requireUserVerification && !$userVerified) {
|
||||
throw new WebAuthnException('user not verified during authentication', WebAuthnException::USER_VERIFICATED);
|
||||
}
|
||||
|
||||
$signCount = $attestationObject->getAuthenticatorData()->getSignCount();
|
||||
if ($signCount > 0) {
|
||||
$this->_signatureCounter = $signCount;
|
||||
}
|
||||
|
||||
// prepare data to store for future logins
|
||||
$data = new \stdClass();
|
||||
$data->rpId = $this->_rpId;
|
||||
$data->attestationFormat = $attestationObject->getAttestationFormatName();
|
||||
$data->credentialId = $attestationObject->getAuthenticatorData()->getCredentialId();
|
||||
$data->credentialPublicKey = $attestationObject->getAuthenticatorData()->getPublicKeyPem();
|
||||
$data->certificateChain = $attestationObject->getCertificateChain();
|
||||
$data->certificate = $attestationObject->getCertificatePem();
|
||||
$data->certificateIssuer = $attestationObject->getCertificateIssuer();
|
||||
$data->certificateSubject = $attestationObject->getCertificateSubject();
|
||||
$data->signatureCounter = $this->_signatureCounter;
|
||||
$data->AAGUID = $attestationObject->getAuthenticatorData()->getAAGUID();
|
||||
$data->rootValid = $rootValid;
|
||||
$data->userPresent = $userPresent;
|
||||
$data->userVerified = $userVerified;
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* process a get request
|
||||
* @param string $clientDataJSON binary from browser
|
||||
* @param string $authenticatorData binary from browser
|
||||
* @param string $signature binary from browser
|
||||
* @param string $credentialPublicKey string PEM-formated public key from used credentialId
|
||||
* @param string|ByteBuffer $challenge binary from used challange
|
||||
* @param int $prevSignatureCnt signature count value of the last login
|
||||
* @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin)
|
||||
* @param bool $requireUserPresent true, if the device must check user presence (e.g. by pressing a button)
|
||||
* @return boolean true if get is successful
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, $prevSignatureCnt=null, $requireUserVerification=false, $requireUserPresent=true) {
|
||||
$authenticatorObj = new Attestation\AuthenticatorData($authenticatorData);
|
||||
$clientDataHash = \hash('sha256', $clientDataJSON, true);
|
||||
$clientData = \json_decode($clientDataJSON);
|
||||
$challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge);
|
||||
|
||||
// https://www.w3.org/TR/webauthn/#verifying-assertion
|
||||
|
||||
// 1. If the allowCredentials option was given when this authentication ceremony was initiated,
|
||||
// verify that credential.id identifies one of the public key credentials that were listed in allowCredentials.
|
||||
// -> TO BE VERIFIED BY IMPLEMENTATION
|
||||
|
||||
// 2. If credential.response.userHandle is present, verify that the user identified
|
||||
// by this value is the owner of the public key credential identified by credential.id.
|
||||
// -> TO BE VERIFIED BY IMPLEMENTATION
|
||||
|
||||
// 3. Using credential’s id attribute (or the corresponding rawId, if base64url encoding is
|
||||
// inappropriate for your use case), look up the corresponding credential public key.
|
||||
// -> TO BE LOOKED UP BY IMPLEMENTATION
|
||||
|
||||
// 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
|
||||
if (!\is_object($clientData)) {
|
||||
throw new WebAuthnException('invalid client data', WebAuthnException::INVALID_DATA);
|
||||
}
|
||||
|
||||
// 7. Verify that the value of C.type is the string webauthn.get.
|
||||
if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.get') {
|
||||
throw new WebAuthnException('invalid type', WebAuthnException::INVALID_TYPE);
|
||||
}
|
||||
|
||||
// 8. Verify that the value of C.challenge matches the challenge that was sent to the
|
||||
// authenticator in the PublicKeyCredentialRequestOptions passed to the get() call.
|
||||
if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
|
||||
throw new WebAuthnException('invalid challenge', WebAuthnException::INVALID_CHALLENGE);
|
||||
}
|
||||
|
||||
// 9. Verify that the value of C.origin matches the Relying Party's origin.
|
||||
if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
|
||||
throw new WebAuthnException('invalid origin', WebAuthnException::INVALID_ORIGIN);
|
||||
}
|
||||
|
||||
// 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
|
||||
if ($authenticatorObj->getRpIdHash() !== $this->_rpIdHash) {
|
||||
throw new WebAuthnException('invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
|
||||
}
|
||||
|
||||
// 12. Verify that the User Present bit of the flags in authData is set
|
||||
if ($requireUserPresent && !$authenticatorObj->getUserPresent()) {
|
||||
throw new WebAuthnException('user not present during authentication', WebAuthnException::USER_PRESENT);
|
||||
}
|
||||
|
||||
// 13. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set.
|
||||
if ($requireUserVerification && !$authenticatorObj->getUserVerified()) {
|
||||
throw new WebAuthnException('user not verificated during authentication', WebAuthnException::USER_VERIFICATED);
|
||||
}
|
||||
|
||||
// 14. Verify the values of the client extension outputs
|
||||
// (extensions not implemented)
|
||||
|
||||
// 16. Using the credential public key looked up in step 3, verify that sig is a valid signature
|
||||
// over the binary concatenation of authData and hash.
|
||||
$dataToVerify = '';
|
||||
$dataToVerify .= $authenticatorData;
|
||||
$dataToVerify .= $clientDataHash;
|
||||
|
||||
$publicKey = \openssl_pkey_get_public($credentialPublicKey);
|
||||
if ($publicKey === false) {
|
||||
throw new WebAuthnException('public key invalid', WebAuthnException::INVALID_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
if (\openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256) !== 1) {
|
||||
throw new WebAuthnException('invalid signature', WebAuthnException::INVALID_SIGNATURE);
|
||||
}
|
||||
|
||||
// 17. If the signature counter value authData.signCount is nonzero,
|
||||
// if less than or equal to the signature counter value stored,
|
||||
// is a signal that the authenticator may be cloned
|
||||
$signatureCounter = $authenticatorObj->getSignCount();
|
||||
if ($signatureCounter > 0) {
|
||||
$this->_signatureCounter = $signatureCounter;
|
||||
if ($prevSignatureCnt !== null && $prevSignatureCnt >= $signatureCounter) {
|
||||
throw new WebAuthnException('signature counter not valid', WebAuthnException::SIGNATURE_COUNTER);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads root certificates from FIDO Alliance Metadata Service (MDS) to a specific folder
|
||||
* https://fidoalliance.org/metadata/
|
||||
* @param string $certFolder Folder path to save the certificates in PEM format.
|
||||
* @param bool $deleteCerts=true
|
||||
* @return int number of cetificates
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
public function queryFidoMetaDataService($certFolder, $deleteCerts=true) {
|
||||
$url = 'https://mds.fidoalliance.org/';
|
||||
$raw = null;
|
||||
if (\function_exists('curl_init')) {
|
||||
$ch = \curl_init($url);
|
||||
\curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
\curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
\curl_setopt($ch, CURLOPT_USERAGENT, 'github.com/lbuchs/WebAuthn - A simple PHP WebAuthn server library');
|
||||
$raw = \curl_exec($ch);
|
||||
\curl_close($ch);
|
||||
} else {
|
||||
$raw = \file_get_contents($url);
|
||||
}
|
||||
|
||||
$certFolder = \rtrim(\realpath($certFolder), '\\/');
|
||||
if (!is_dir($certFolder)) {
|
||||
throw new WebAuthnException('Invalid folder path for query FIDO Alliance Metadata Service');
|
||||
}
|
||||
|
||||
if (!\is_string($raw)) {
|
||||
throw new WebAuthnException('Unable to query FIDO Alliance Metadata Service');
|
||||
}
|
||||
|
||||
$jwt = \explode('.', $raw);
|
||||
if (\count($jwt) !== 3) {
|
||||
throw new WebAuthnException('Invalid JWT from FIDO Alliance Metadata Service');
|
||||
}
|
||||
|
||||
if ($deleteCerts) {
|
||||
foreach (\scandir($certFolder) as $ca) {
|
||||
if (\substr($ca, -4) === '.pem') {
|
||||
if (\unlink($certFolder . DIRECTORY_SEPARATOR . $ca) === false) {
|
||||
throw new WebAuthnException('Cannot delete certs in folder for FIDO Alliance Metadata Service');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list($header, $payload, $hash) = $jwt;
|
||||
$payload = Binary\ByteBuffer::fromBase64Url($payload)->getJson();
|
||||
|
||||
$count = 0;
|
||||
if (\is_object($payload) && \property_exists($payload, 'entries') && \is_array($payload->entries)) {
|
||||
foreach ($payload->entries as $entry) {
|
||||
if (\is_object($entry) && \property_exists($entry, 'metadataStatement') && \is_object($entry->metadataStatement)) {
|
||||
$description = $entry->metadataStatement->description ?? null;
|
||||
$attestationRootCertificates = $entry->metadataStatement->attestationRootCertificates ?? null;
|
||||
|
||||
if ($description && $attestationRootCertificates) {
|
||||
|
||||
// create filename
|
||||
$certFilename = \preg_replace('/[^a-z0-9]/i', '_', $description);
|
||||
$certFilename = \trim(\preg_replace('/\_{2,}/i', '_', $certFilename),'_') . '.pem';
|
||||
$certFilename = \strtolower($certFilename);
|
||||
|
||||
// add certificate
|
||||
$certContent = $description . "\n";
|
||||
$certContent .= \str_repeat('-', \mb_strlen($description)) . "\n";
|
||||
|
||||
foreach ($attestationRootCertificates as $attestationRootCertificate) {
|
||||
$count++;
|
||||
$certContent .= "\n-----BEGIN CERTIFICATE-----\n";
|
||||
$certContent .= \chunk_split(\trim($attestationRootCertificate), 64, "\n");
|
||||
$certContent .= "-----END CERTIFICATE-----\n";
|
||||
}
|
||||
|
||||
if (\file_put_contents($certFolder . DIRECTORY_SEPARATOR . $certFilename, $certContent) === false) {
|
||||
throw new WebAuthnException('unable to save certificate from FIDO Alliance Metadata Service');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
// PRIVATE
|
||||
// -----------------------------------------------
|
||||
|
||||
/**
|
||||
* checks if the origin matchs the RP ID
|
||||
* @param string $origin
|
||||
* @return boolean
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function _checkOrigin($origin) {
|
||||
// https://www.w3.org/TR/webauthn/#rp-id
|
||||
|
||||
// The origin's scheme must be https
|
||||
if ($this->_rpId !== 'localhost' && \parse_url($origin, PHP_URL_SCHEME) !== 'https') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// extract host from origin
|
||||
$host = \parse_url($origin, PHP_URL_HOST);
|
||||
$host = \trim($host, '.');
|
||||
|
||||
// The RP ID must be equal to the origin's effective domain, or a registrable
|
||||
// domain suffix of the origin's effective domain.
|
||||
return \preg_match('/' . \preg_quote($this->_rpId) . '$/i', $host) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* generates a new challange
|
||||
* @param int $length
|
||||
* @return string
|
||||
* @throws WebAuthnException
|
||||
*/
|
||||
private function _createChallenge($length = 32) {
|
||||
if (!$this->_challenge) {
|
||||
$this->_challenge = ByteBuffer::randomBuffer($length);
|
||||
}
|
||||
return $this->_challenge;
|
||||
}
|
||||
}
|
||||
27
data/web/inc/lib/WebAuthn/WebAuthnException.php
Executable file
27
data/web/inc/lib/WebAuthn/WebAuthnException.php
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
namespace lbuchs\WebAuthn;
|
||||
|
||||
/**
|
||||
* @author Lukas Buchs
|
||||
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
|
||||
*/
|
||||
class WebAuthnException extends \Exception {
|
||||
const INVALID_DATA = 1;
|
||||
const INVALID_TYPE = 2;
|
||||
const INVALID_CHALLENGE = 3;
|
||||
const INVALID_ORIGIN = 4;
|
||||
const INVALID_RELYING_PARTY = 5;
|
||||
const INVALID_SIGNATURE = 6;
|
||||
const INVALID_PUBLIC_KEY = 7;
|
||||
const CERTIFICATE_NOT_TRUSTED = 8;
|
||||
const USER_PRESENT = 9;
|
||||
const USER_VERIFICATED = 10;
|
||||
const SIGNATURE_COUNTER = 11;
|
||||
const CRYPTO_STRONG = 13;
|
||||
const BYTEBUFFER = 14;
|
||||
const CBOR = 15;
|
||||
|
||||
public function __construct($message = "", $code = 0, $previous = null) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
48
data/web/inc/lib/WebAuthn/rootCertificates/apple.pem
Executable file
48
data/web/inc/lib/WebAuthn/rootCertificates/apple.pem
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number:
|
||||
68:1d:01:6c:7a:3c:e3:02:25:a5:01:94:28:47:57:71
|
||||
|
||||
Signature Algorithm: ecdsa-with-SHA384
|
||||
|
||||
Issuer:
|
||||
stateOrProvinceName = California
|
||||
organizationName = Apple Inc.
|
||||
commonName = Apple WebAuthn Root CA
|
||||
|
||||
Validity
|
||||
Not Before: Mar 18 18:21:32 2020 GMT
|
||||
Not After : Mar 15 00:00:00 2045 GMT
|
||||
|
||||
Subject:
|
||||
stateOrProvinceName = California
|
||||
organizationName = Apple Inc.
|
||||
commonName = Apple WebAuthn Root CA
|
||||
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: id-ecPublicKey
|
||||
ASN1 OID: secp384r1
|
||||
|
||||
X509v3 extensions:
|
||||
X509v3 Basic Constraints: critical
|
||||
CA:TRUE
|
||||
X509v3 Subject Key Identifier:
|
||||
26:D7:64:D9:C5:78:C2:5A:67:D1:A7:DE:6B:12:D0:1B:63:F1:C6:D7
|
||||
X509v3 Key Usage: critical
|
||||
Certificate Sign, CRL Sign
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
|
||||
HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
|
||||
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
|
||||
NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
|
||||
A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
|
||||
AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
|
||||
xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
|
||||
pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
|
||||
2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
|
||||
MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
|
||||
jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
|
||||
1bWeT0vT
|
||||
-----END CERTIFICATE-----
|
||||
BIN
data/web/inc/lib/WebAuthn/rootCertificates/bsi.pem
Executable file
BIN
data/web/inc/lib/WebAuthn/rootCertificates/bsi.pem
Executable file
Binary file not shown.
37
data/web/inc/lib/WebAuthn/rootCertificates/globalSign.pem
Executable file
37
data/web/inc/lib/WebAuthn/rootCertificates/globalSign.pem
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number:
|
||||
04:00:00:00:00:01:0f:86:26:e6:0d
|
||||
Signature Algorithm: sha1WithRSAEncryption
|
||||
Issuer: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
|
||||
Validity
|
||||
Not Before: Dec 15 08:00:00 2006 GMT
|
||||
Not After : Dec 15 08:00:00 2021 GMT
|
||||
Subject: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (2048 bit)
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
|
||||
A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
|
||||
Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
|
||||
MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
|
||||
A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
|
||||
v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
|
||||
eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
|
||||
tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
|
||||
C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
|
||||
zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
|
||||
mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
|
||||
V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
|
||||
bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
|
||||
3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
|
||||
J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
|
||||
291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
|
||||
ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
|
||||
AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
|
||||
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
|
||||
-----END CERTIFICATE-----
|
||||
130
data/web/inc/lib/WebAuthn/rootCertificates/googleHardware.pem
Executable file
130
data/web/inc/lib/WebAuthn/rootCertificates/googleHardware.pem
Executable file
|
|
@ -0,0 +1,130 @@
|
|||
Google Hardware Attestation Root certificate
|
||||
----------------------------------------------
|
||||
|
||||
https://developer.android.com/training/articles/security-key-attestation.html
|
||||
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number:
|
||||
e8:fa:19:63:14:d2:fa:18
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: serialNumber = f92009e853b6b045
|
||||
Validity
|
||||
Not Before: May 26 16:28:52 2016 GMT
|
||||
Not After : May 24 16:28:52 2026 GMT
|
||||
Subject: serialNumber = f92009e853b6b045
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (4096 bit)
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Subject Key Identifier:
|
||||
36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||
X509v3 Authority Key Identifier:
|
||||
keyid:36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||
|
||||
X509v3 Basic Constraints: critical
|
||||
CA:TRUE
|
||||
X509v3 Key Usage: critical
|
||||
Digital Signature, Certificate Sign, CRL Sign
|
||||
X509v3 CRL Distribution Points:
|
||||
|
||||
Full Name:
|
||||
URI:https://android.googleapis.com/attestation/crl/
|
||||
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYy
|
||||
ODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
|
||||
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
|
||||
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
|
||||
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
|
||||
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
|
||||
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
|
||||
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
|
||||
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
|
||||
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
|
||||
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
|
||||
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
|
||||
AGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYD
|
||||
VR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAO
|
||||
BgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lk
|
||||
Lmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQAD
|
||||
ggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfB
|
||||
Pb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00m
|
||||
qC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rY
|
||||
DBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPm
|
||||
QUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4u
|
||||
JU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyD
|
||||
CdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79Iy
|
||||
ZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxD
|
||||
qwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23Uaic
|
||||
MDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1
|
||||
wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number: 15352756130135856819 (0xd50ff25ba3f2d6b3)
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer:
|
||||
serialNumber = f92009e853b6b045
|
||||
Validity
|
||||
Not Before: Nov 22 20:37:58 2019 GMT
|
||||
Not After : Nov 18 20:37:58 2034 GMT
|
||||
Subject:
|
||||
serialNumber = f92009e853b6b045
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (4096 bit)
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Subject Key Identifier:
|
||||
36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||
X509v3 Authority Key Identifier:
|
||||
keyid:36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
|
||||
|
||||
X509v3 Basic Constraints: critical
|
||||
CA:TRUE
|
||||
X509v3 Key Usage: critical
|
||||
Certificate Sign
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFHDCCAwSgAwIBAgIJANUP8luj8tazMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTkxMTIyMjAzNzU4WhcNMzQxMTE4MjAz
|
||||
NzU4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
|
||||
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
|
||||
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
|
||||
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
|
||||
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
|
||||
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
|
||||
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
|
||||
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
|
||||
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
|
||||
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
|
||||
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
|
||||
AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
|
||||
IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
|
||||
VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBOMaBc8oumXb2voc7XCWnu
|
||||
XKhBBK3e2KMGz39t7lA3XXRe2ZLLAkLM5y3J7tURkf5a1SutfdOyXAmeE6SRo83U
|
||||
h6WszodmMkxK5GM4JGrnt4pBisu5igXEydaW7qq2CdC6DOGjG+mEkN8/TA6p3cno
|
||||
L/sPyz6evdjLlSeJ8rFBH6xWyIZCbrcpYEJzXaUOEaxxXxgYz5/cTiVKN2M1G2ok
|
||||
QBUIYSY6bjEL4aUN5cfo7ogP3UvliEo3Eo0YgwuzR2v0KR6C1cZqZJSTnghIC/vA
|
||||
D32KdNQ+c3N+vl2OTsUVMC1GiWkngNx1OO1+kXW+YTnnTUOtOIswUP/Vqd5SYgAI
|
||||
mMAfY8U9/iIgkQj6T2W6FsScy94IN9fFhE1UtzmLoBIuUFsVXJMTz+Jucth+IqoW
|
||||
Fua9v1R93/k98p41pjtFX+H8DslVgfP097vju4KDlqN64xV1grw3ZLl4CiOe/A91
|
||||
oeLm2UHOq6wn3esB4r2EIQKb6jTVGu5sYCcdWpXr0AUVqcABPdgL+H7qJguBw09o
|
||||
jm6xNIrw2OocrDKsudk/okr/AwqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUB
|
||||
ZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCH
|
||||
ex0SdDrx+tWUDqG8At2JHA==
|
||||
-----END CERTIFICATE-----
|
||||
31
data/web/inc/lib/WebAuthn/rootCertificates/huawei.pem
Executable file
31
data/web/inc/lib/WebAuthn/rootCertificates/huawei.pem
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIFZDCCA0ygAwIBAgIIYsLLTehAXpYwDQYJKoZIhvcNAQELBQAwUDELMAkGA1UE
|
||||
BhMCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UECwwKSHVhd2VpIENCRzEbMBkG
|
||||
A1UEAwwSSHVhd2VpIENCRyBSb290IENBMB4XDTE3MDgyMTEwNTYyN1oXDTQyMDgx
|
||||
NTEwNTYyN1owUDELMAkGA1UEBhMCQ04xDzANBgNVBAoMBkh1YXdlaTETMBEGA1UE
|
||||
CwwKSHVhd2VpIENCRzEbMBkGA1UEAwwSSHVhd2VpIENCRyBSb290IENBMIICIjAN
|
||||
BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1OyKm3Ig/6eibB7Uz2o93UqGk2M7
|
||||
84WdfF8mvffvu218d61G5M3Px54E3kefUTk5Ky1ywHvw7Rp9KDuYv7ktaHkk+yr5
|
||||
9Ihseu3a7iM/C6SnMSGt+LfB/Bcob9Abw95EigXQ4yQddX9hbNrin3AwZw8wMjEI
|
||||
SYYDo5GuYDL0NbAiYg2Y5GpfYIqRzoi6GqDz+evLrsl20kJeCEPgJZN4Jg00Iq9k
|
||||
++EKOZ5Jc/Zx22ZUgKpdwKABkvzshEgG6WWUPB+gosOiLv++inu/9blDpEzQZhjZ
|
||||
9WVHpURHDK1YlCvubVAMhDpnbqNHZ0AxlPletdoyugrH/OLKl5inhMXNj3Re7Hl8
|
||||
WsBWLUKp6sXFf0dvSFzqnr2jkhicS+K2IYZnjghC9cOBRO8fnkonh0EBt0evjUIK
|
||||
r5ClbCKioBX8JU+d4ldtWOpp2FlxeFTLreDJ5ZBU4//bQpTwYMt7gwMK+MO5Wtok
|
||||
Ux3UF98Z6GdUgbl6nBjBe82c7oIQXhHGHPnURQO7DDPgyVnNOnTPIkmiHJh/e3vk
|
||||
VhiZNHFCCLTip6GoJVrLxwb9i4q+d0thw4doxVJ5NB9OfDMV64/ybJgpf7m3Ld2y
|
||||
E0gsf1prrRlDFDXjlYyqqpf1l9Y0u3ctXo7UpXMgbyDEpUQhq3a7txZQO/17luTD
|
||||
oA6Tz1ADavvBwHkCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
|
||||
MAMBAf8wHQYDVR0OBBYEFKrE03lH6G4ja+/wqWwicz16GWmhMA0GCSqGSIb3DQEB
|
||||
CwUAA4ICAQC1d3TMB+VHZdGrWJbfaBShFNiCTN/MceSHOpzBn6JumQP4N7mxCOwd
|
||||
RSsGKQxV2NPH7LTXWNhUvUw5Sek96FWx/+Oa7jsj3WNAVtmS3zKpCQ5iGb08WIRO
|
||||
cFnx3oUQ5rcO8r/lUk7Q2cN0E+rF4xsdQrH9k2cd3kAXZXBjfxfKPJTdPy1XnZR/
|
||||
h8H5EwEK5DWjSzK1wKd3G/Fxdm3E23pcr4FZgdYdOlFSiqW2TJ3Qe6lF4GOKOOyd
|
||||
WHkpu54ieTsqoYcuMKnKMjT2SLNNgv9Gu5ipaG8Olz6g9C7Htp943lmK/1Vtnhgg
|
||||
pL3rDTsFX/+ehk7OtxuNzRMD9lXUtEfok7f8XB0dcL4ZjnEhDmp5QZqC1kMubHQt
|
||||
QnTauEiv0YkSGOwJAUZpK1PIff5GgxXYfaHfBC6Op4q02ppl5Q3URl7XIjYLjvs9
|
||||
t4S9xPe8tb6416V2fe1dZ62vOXMMKHkZjVihh+IceYpJYHuyfKoYJyahLOQXZykG
|
||||
K5iPAEEtq3HPfMVF43RKHOwfhrAH5KwelUA/0EkcR4Gzth1MKEqojdnYNemkkSy7
|
||||
aNPPT4LEm5R7sV6vG1CjwbgvQrWCgc4nMb8ngdfnVF7Ydqjqi9SAqUzIk4+Uf0ZY
|
||||
+6RY5IcHdCaiPaWIE1xURQ8B0DRUURsQwXdjZhgLN/DKJpCl5aCCxg==
|
||||
-----END CERTIFICATE-----
|
||||
56
data/web/inc/lib/WebAuthn/rootCertificates/hypersecu.pem
Executable file
56
data/web/inc/lib/WebAuthn/rootCertificates/hypersecu.pem
Executable file
|
|
@ -0,0 +1,56 @@
|
|||
HyperFIDO U2F Security Key Attestation CA
|
||||
https://hypersecu.com/support/downloads/attestation
|
||||
|
||||
Last Update: 2017-01-01
|
||||
|
||||
HyperFIDO U2F Security Key devices which contain attestation certificates signed by a set of CAs.
|
||||
This file contains the CA certificates that Relying Parties (RP) need to configure their software
|
||||
with to be able to verify U2F device certificates.
|
||||
|
||||
The file will be updated as needed when we publish more CA certificates.
|
||||
|
||||
Issuer: CN=FT FIDO 0100
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBjTCCATOgAwIBAgIBATAKBggqhkjOPQQDAjAXMRUwEwYDVQQDEwxGVCBGSURP
|
||||
IDAxMDAwHhcNMTQwNzAxMTUzNjI2WhcNNDQwNzAzMTUzNjI2WjAXMRUwEwYDVQQD
|
||||
EwxGVCBGSURPIDAxMDAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASxdLxJx8ol
|
||||
S3DS5cIHzunPF0gg69d+o8ZVCMJtpRtlfBzGuVL4YhaXk2SC2gptPTgmpZCV2vbN
|
||||
fAPi5gOF0vbZo3AwbjAdBgNVHQ4EFgQUXt4jWlYDgwhaPU+EqLmeM9LoPRMwPwYD
|
||||
VR0jBDgwNoAUXt4jWlYDgwhaPU+EqLmeM9LoPROhG6QZMBcxFTATBgNVBAMTDEZU
|
||||
IEZJRE8gMDEwMIIBATAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIQC2
|
||||
D9o9cconKTo8+4GZPyZBJ3amc8F0/kzyidX9dhrAIAIgM9ocs5BW/JfmshVP9Mb+
|
||||
Joa/kgX4dWbZxrk0ioTfJZg=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number: 4107 (0x100b)
|
||||
Signature Algorithm: ecdsa-with-SHA256
|
||||
Issuer:
|
||||
commonName = HYPERFIDO 0200
|
||||
organizationName = HYPERSECU
|
||||
countryName = CA
|
||||
Validity
|
||||
Not Before: Jan 1 00:00:00 2018 GMT
|
||||
Not After : Dec 31 23:59:59 2047 GMT
|
||||
Subject:
|
||||
commonName = HYPERFIDO 0200
|
||||
organizationName = HYPERSECU
|
||||
countryName = CA
|
||||
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBxzCCAWygAwIBAgICEAswCgYIKoZIzj0EAwIwOjELMAkGA1UEBhMCQ0ExEjAQ
|
||||
BgNVBAoMCUhZUEVSU0VDVTEXMBUGA1UEAwwOSFlQRVJGSURPIDAyMDAwIBcNMTgw
|
||||
MTAxMDAwMDAwWhgPMjA0NzEyMzEyMzU5NTlaMDoxCzAJBgNVBAYTAkNBMRIwEAYD
|
||||
VQQKDAlIWVBFUlNFQ1UxFzAVBgNVBAMMDkhZUEVSRklETyAwMjAwMFkwEwYHKoZI
|
||||
zj0CAQYIKoZIzj0DAQcDQgAErKUI1G0S7a6IOLlmHipLlBuxTYjsEESQvzQh3dB7
|
||||
dvxxWWm7kWL91rq6S7ayZG0gZPR+zYqdFzwAYDcG4+aX66NgMF4wHQYDVR0OBBYE
|
||||
FLZYcfMMwkQAGbt3ryzZFPFypmsIMB8GA1UdIwQYMBaAFLZYcfMMwkQAGbt3ryzZ
|
||||
FPFypmsIMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMC
|
||||
A0kAMEYCIQCG2/ppMGt7pkcRie5YIohS3uDPIrmiRcTjqDclKVWg0gIhANcPNDZH
|
||||
E2/zZ+uB5ThG9OZus+xSb4knkrbAyXKX2zm/
|
||||
-----END CERTIFICATE-----
|
||||
28844
data/web/inc/lib/WebAuthn/rootCertificates/microsoftTpmCollection.pem
Executable file
28844
data/web/inc/lib/WebAuthn/rootCertificates/microsoftTpmCollection.pem
Executable file
File diff suppressed because it is too large
Load diff
11
data/web/inc/lib/WebAuthn/rootCertificates/nitro.pem
Executable file
11
data/web/inc/lib/WebAuthn/rootCertificates/nitro.pem
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBmjCCAT8CFBZiBJbp2fT/LaRJ8Xwl9qhX62boMAoGCCqGSM49BAMCME4xCzAJ
|
||||
BgNVBAYTAkRFMRYwFAYDVQQKDA1OaXRyb2tleSBHbWJIMRAwDgYDVQQLDAdSb290
|
||||
IENBMRUwEwYDVQQDDAxuaXRyb2tleS5jb20wIBcNMTkxMjA0MDczNTM1WhgPMjA2
|
||||
OTExMjEwNzM1MzVaME4xCzAJBgNVBAYTAkRFMRYwFAYDVQQKDA1OaXRyb2tleSBH
|
||||
bWJIMRAwDgYDVQQLDAdSb290IENBMRUwEwYDVQQDDAxuaXRyb2tleS5jb20wWTAT
|
||||
BgcqhkjOPQIBBggqhkjOPQMBBwNCAAQy6KIN2gXqaSMWdWir/Hnx58NBzjthYdNv
|
||||
k95hdt7jCpyW2cHqLdQ5Sqcvo0CuordgDOach0ZGB60w9GZY8SHJMAoGCCqGSM49
|
||||
BAMCA0kAMEYCIQDLmdy2G2mM4rZKjl6CVfjV7khilIS5D3xRQzubeqzQNAIhAKIG
|
||||
X29SfiB6K9k6Hb3q+q7bRn1o1dhV1cj592YYnu1/
|
||||
-----END CERTIFICATE-----
|
||||
41
data/web/inc/lib/WebAuthn/rootCertificates/solo.pem
Executable file
41
data/web/inc/lib/WebAuthn/rootCertificates/solo.pem
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
Solokeys FIDO2/U2F Device Attestation CA
|
||||
========================================
|
||||
Data:
|
||||
Version: 1 (0x0)
|
||||
Serial Number: 14143382635911888524 (0xc44763928ff4be8c)
|
||||
Signature Algorithm: ecdsa-with-SHA256
|
||||
|
||||
Issuer:
|
||||
emailAddress = hello@solokeys.com
|
||||
commonName = solokeys.com
|
||||
organizationalUnitName = Root CA
|
||||
organizationName = Solo Keys
|
||||
stateOrProvinceName = Maryland
|
||||
countryName = US
|
||||
|
||||
Validity
|
||||
Not Before: Nov 11 12:51:42 2018 GMT
|
||||
Not After : Oct 29 12:51:42 2068 GMT
|
||||
|
||||
Subject:
|
||||
emailAddress = hello@solokeys.com
|
||||
commonName = solokeys.com
|
||||
organizationalUnitName = Root CA
|
||||
organizationName = Solo Keys
|
||||
stateOrProvinceName = Maryland
|
||||
countryName = US
|
||||
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB9DCCAZoCCQDER2OSj/S+jDAKBggqhkjOPQQDAjCBgDELMAkGA1UEBhMCVVMx
|
||||
ETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQKDAlTb2xvIEtleXMxEDAOBgNVBAsM
|
||||
B1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlzLmNvbTEhMB8GCSqGSIb3DQEJARYS
|
||||
aGVsbG9Ac29sb2tleXMuY29tMCAXDTE4MTExMTEyNTE0MloYDzIwNjgxMDI5MTI1
|
||||
MTQyWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQK
|
||||
DAlTb2xvIEtleXMxEDAOBgNVBAsMB1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlz
|
||||
LmNvbTEhMB8GCSqGSIb3DQEJARYSaGVsbG9Ac29sb2tleXMuY29tMFkwEwYHKoZI
|
||||
zj0CAQYIKoZIzj0DAQcDQgAEWHAN0CCJVZdMs0oktZ5m93uxmB1iyq8ELRLtqVFL
|
||||
SOiHQEab56qRTB/QzrpGAY++Y2mw+vRuQMNhBiU0KzwjBjAKBggqhkjOPQQDAgNI
|
||||
ADBFAiEAz9SlrAXIlEu87vra54rICPs+4b0qhp3PdzcTg7rvnP0CIGjxzlteQQx+
|
||||
jQGd7rwSZuE5RWUPVygYhUstQO9zNUOs
|
||||
-----END CERTIFICATE-----
|
||||
17
data/web/inc/lib/WebAuthn/rootCertificates/trustkey.pem
Executable file
17
data/web/inc/lib/WebAuthn/rootCertificates/trustkey.pem
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICpTCCAkqgAwIBAgIBATAKBggqhkjOPQQDAjCBrzELMAkGA1UEBhMCS1IxETAP
|
||||
BgNVBAgMCFNlb3VsLVNpMRMwEQYDVQQHDApHYW5nbmFtLUd1MRcwFQYDVQQKDA5l
|
||||
V0JNIENvLiwgTHRkLjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlv
|
||||
bjEcMBoGA1UEAwwTZVdCTSBDQSBDZXJ0aWZpY2F0ZTEdMBsGCSqGSIb3DQEJARYO
|
||||
aW5mb0BlLXdibS5jb20wHhcNMTgwNzAyMDUzMTM5WhcNMjMwNzAxMDUzMTM5WjCB
|
||||
rzELMAkGA1UEBhMCS1IxETAPBgNVBAgMCFNlb3VsLVNpMRMwEQYDVQQHDApHYW5n
|
||||
bmFtLUd1MRcwFQYDVQQKDA5lV0JNIENvLiwgTHRkLjEiMCAGA1UECwwZQXV0aGVu
|
||||
dGljYXRvciBBdHRlc3RhdGlvbjEcMBoGA1UEAwwTZVdCTSBDQSBDZXJ0aWZpY2F0
|
||||
ZTEdMBsGCSqGSIb3DQEJARYOaW5mb0BlLXdibS5jb20wWTATBgcqhkjOPQIBBggq
|
||||
hkjOPQMBBwNCAAQIfqHisi0oO/eyOqSaDrr9itG2IymBkHnSDGQIIYmT+vqA8AgO
|
||||
81momc2Ld5PGpEN6muE54wPHQjvc/yCih8u2o1UwUzASBgNVHRMBAf8ECDAGAQH/
|
||||
AgEAMB0GA1UdDgQWBBS3J/fxiAv22irdBs98SODhF7kU/jALBgNVHQ8EBAMCAQYw
|
||||
EQYJYIZIAYb4QgEBBAQDAgAHMAoGCCqGSM49BAMCA0kAMEYCIQDc41LFK4LJCBU2
|
||||
VVKIz7Z6sxPhUEkh8nLSLK6IXdkP5wIhAIeKVOZchaVO5aF7fbdXoSrcyy1YYeUe
|
||||
PLojcKI9fX84
|
||||
-----END CERTIFICATE-----
|
||||
42
data/web/inc/lib/WebAuthn/rootCertificates/yubico.pem
Executable file
42
data/web/inc/lib/WebAuthn/rootCertificates/yubico.pem
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
Yubico U2F Device Attestation CA
|
||||
================================
|
||||
|
||||
Last Update: 2014-09-01
|
||||
|
||||
Yubico manufacturer U2F devices that contains device attestation
|
||||
certificates signed by a set of Yubico CAs. This file contains the CA
|
||||
certificates that Relying Parties (RP) need to configure their
|
||||
software with to be able to verify U2F device certificates.
|
||||
|
||||
This file has been signed with OpenPGP and you should verify the
|
||||
signature and the authenticity of the public key before trusting the
|
||||
content. The signature is located next to the file:
|
||||
|
||||
https://developers.yubico.com/u2f/yubico-u2f-ca-certs.txt
|
||||
https://developers.yubico.com/u2f/yubico-u2f-ca-certs.txt.sig
|
||||
|
||||
We will update this file from time to time when we publish more CA
|
||||
certificates.
|
||||
|
||||
Name: Yubico U2F Root CA Serial 457200631
|
||||
Issued: 2014-08-01
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ
|
||||
dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw
|
||||
MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290
|
||||
IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
|
||||
AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk
|
||||
5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep
|
||||
8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw
|
||||
nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT
|
||||
9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw
|
||||
LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ
|
||||
hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN
|
||||
BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4
|
||||
MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt
|
||||
hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k
|
||||
LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U
|
||||
sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc
|
||||
U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==
|
||||
-----END CERTIFICATE-----
|
||||
500
data/web/inc/lib/Yubico.php
Executable file
500
data/web/inc/lib/Yubico.php
Executable file
|
|
@ -0,0 +1,500 @@
|
|||
<?php
|
||||
/**
|
||||
* Class for verifying Yubico One-Time-Passcodes
|
||||
*
|
||||
* @category Auth
|
||||
* @package Auth_Yubico
|
||||
* @author Simon Josefsson <simon@yubico.com>, Olov Danielson <olov@yubico.com>
|
||||
* @copyright 2007-2020 Yubico AB
|
||||
* @license https://opensource.org/licenses/bsd-license.php New BSD License
|
||||
* @version 2.0
|
||||
* @link https://www.yubico.com/
|
||||
*/
|
||||
|
||||
require_once 'PEAR.php';
|
||||
|
||||
/**
|
||||
* Class for verifying Yubico One-Time-Passcodes
|
||||
*
|
||||
* Simple example:
|
||||
* <code>
|
||||
* require_once 'Auth/Yubico.php';
|
||||
* $otp = "ccbbddeertkrctjkkcglfndnlihhnvekchkcctif";
|
||||
*
|
||||
* # Generate a new id+key from https://api.yubico.com/get-api-key/
|
||||
* $yubi = new Auth_Yubico('42', 'FOOBAR=');
|
||||
* $auth = $yubi->verify($otp);
|
||||
* if (PEAR::isError($auth)) {
|
||||
* print "<p>Authentication failed: " . $auth->getMessage();
|
||||
* print "<p>Debug output from server: " . $yubi->getLastResponse();
|
||||
* } else {
|
||||
* print "<p>You are authenticated!";
|
||||
* }
|
||||
* </code>
|
||||
*/
|
||||
class Auth_Yubico
|
||||
{
|
||||
/**#@+
|
||||
* @access private
|
||||
*/
|
||||
|
||||
/**
|
||||
* Yubico client ID
|
||||
* @var string
|
||||
*/
|
||||
var $_id;
|
||||
|
||||
/**
|
||||
* Yubico client key
|
||||
* @var string
|
||||
*/
|
||||
var $_key;
|
||||
|
||||
/**
|
||||
* List with URL part of validation servers
|
||||
* @var array
|
||||
*/
|
||||
var $_url_list;
|
||||
|
||||
/**
|
||||
* index to _url_list
|
||||
* @var int
|
||||
*/
|
||||
var $_url_index;
|
||||
|
||||
/**
|
||||
* Last query to server
|
||||
* @var string
|
||||
*/
|
||||
var $_lastquery;
|
||||
|
||||
/**
|
||||
* Response from server
|
||||
* @var string
|
||||
*/
|
||||
var $_response;
|
||||
|
||||
/**
|
||||
* Number of times we retried in our last validation
|
||||
* @var int
|
||||
*/
|
||||
var $_retries;
|
||||
|
||||
/**
|
||||
* Flag whether to verify HTTPS server certificates or not.
|
||||
* @var boolean
|
||||
*/
|
||||
var $_httpsverify;
|
||||
|
||||
/**
|
||||
* Maximum number of times we will retry transient HTTP errors
|
||||
* @var int
|
||||
*/
|
||||
var $_max_retries;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* Sets up the object
|
||||
* @param string $id The client identity
|
||||
* @param string $key The client MAC key (optional)
|
||||
* @param boolean $https noop
|
||||
* @param boolean $httpsverify Flag whether to use verify HTTPS
|
||||
* server certificates (optional,
|
||||
* default true)
|
||||
* @access public
|
||||
*/
|
||||
public function __construct($id, $key = '', $https = 0, $httpsverify = 1, $max_retries = 3)
|
||||
{
|
||||
$this->_id = $id;
|
||||
$this->_key = base64_decode($key);
|
||||
$this->_httpsverify = $httpsverify;
|
||||
$this->_max_retries = $max_retries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify to use a different URL part for verification.
|
||||
* The default is "https://api.yubico.com/wsapi/2.0/verify".
|
||||
*
|
||||
* @param string $url New server URL part to use
|
||||
* @access public
|
||||
* @deprecated
|
||||
*/
|
||||
function setURLpart($url)
|
||||
{
|
||||
$this->_url_list = array($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next URL part from list to use for validation.
|
||||
*
|
||||
* @return mixed string with URL part or false if no more URLs in list
|
||||
* @access public
|
||||
*/
|
||||
function getNextURLpart()
|
||||
{
|
||||
if ($this->_url_list) $url_list=$this->_url_list;
|
||||
else $url_list=array('https://api.yubico.com/wsapi/2.0/verify');
|
||||
|
||||
if ($this->_url_index>=count($url_list)) return false;
|
||||
else return $url_list[$this->_url_index++];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets index to URL list
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function URLreset()
|
||||
{
|
||||
$this->_url_index=0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add another URLpart.
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function addURLpart($URLpart)
|
||||
{
|
||||
$this->_url_list[]=$URLpart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last query sent to the server, if any.
|
||||
*
|
||||
* @return string Request to server
|
||||
* @access public
|
||||
*/
|
||||
function getLastQuery()
|
||||
{
|
||||
return $this->_lastquery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last data received from the server, if any.
|
||||
*
|
||||
* @return string Output from server
|
||||
* @access public
|
||||
*/
|
||||
function getLastResponse()
|
||||
{
|
||||
return $this->_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of retries that were used in the last validation
|
||||
*
|
||||
* @return int Number of retries
|
||||
* @access public
|
||||
*/
|
||||
function getRetries()
|
||||
{
|
||||
return $this->_retries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse input string into password, yubikey prefix,
|
||||
* ciphertext, and OTP.
|
||||
*
|
||||
* @param string Input string to parse
|
||||
* @param string Optional delimiter re-class, default is '[:]'
|
||||
* @return array Keyed array with fields
|
||||
* @access public
|
||||
*/
|
||||
function parsePasswordOTP($str, $delim = '[:]')
|
||||
{
|
||||
if (!preg_match("/^((.*)" . $delim . ")?" .
|
||||
"(([cbdefghijklnrtuv]{0,16})" .
|
||||
"([cbdefghijklnrtuv]{32}))$/i",
|
||||
$str, $matches)) {
|
||||
/* Dvorak? */
|
||||
if (!preg_match("/^((.*)" . $delim . ")?" .
|
||||
"(([jxe\.uidchtnbpygk]{0,16})" .
|
||||
"([jxe\.uidchtnbpygk]{32}))$/i",
|
||||
$str, $matches)) {
|
||||
return false;
|
||||
} else {
|
||||
$ret['otp'] = strtr($matches[3], "jxe.uidchtnbpygk", "cbdefghijklnrtuv");
|
||||
}
|
||||
} else {
|
||||
$ret['otp'] = $matches[3];
|
||||
}
|
||||
$ret['password'] = $matches[2];
|
||||
$ret['prefix'] = $matches[4];
|
||||
$ret['ciphertext'] = $matches[5];
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/* TODO? Add functions to get parsed parts of server response? */
|
||||
|
||||
/**
|
||||
* Parse parameters from last response
|
||||
*
|
||||
* example: getParameters("timestamp", "sessioncounter", "sessionuse");
|
||||
*
|
||||
* @param array @parameters Array with strings representing
|
||||
* parameters to parse
|
||||
* @return array parameter array from last response
|
||||
* @access public
|
||||
*/
|
||||
function getParameters($parameters)
|
||||
{
|
||||
if ($parameters == null) {
|
||||
$parameters = array('timestamp', 'sessioncounter', 'sessionuse');
|
||||
}
|
||||
$param_array = array();
|
||||
foreach ($parameters as $param) {
|
||||
if(!preg_match("/" . $param . "=([0-9]+)/", $this->_response, $out)) {
|
||||
return PEAR::raiseError('Could not parse parameter ' . $param . ' from response');
|
||||
}
|
||||
$param_array[$param]=$out[1];
|
||||
}
|
||||
return $param_array;
|
||||
}
|
||||
|
||||
function _make_curl_handle($query, $timeout=null)
|
||||
{
|
||||
flush();
|
||||
$handle = curl_init($query);
|
||||
curl_setopt($handle, CURLOPT_USERAGENT, "PEAR Auth_Yubico");
|
||||
curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
|
||||
if (!$this->_httpsverify) {
|
||||
curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
}
|
||||
curl_setopt($handle, CURLOPT_FAILONERROR, true);
|
||||
/* If timeout is set, we better apply it here as well
|
||||
* in case the validation server fails to follow it. */
|
||||
if ($timeout) {
|
||||
curl_setopt($handle, CURLOPT_TIMEOUT, $timeout);
|
||||
}
|
||||
|
||||
return $handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Yubico OTP against multiple URLs
|
||||
* Protocol specification 2.0 is used to construct validation requests
|
||||
*
|
||||
* @param string $token Yubico OTP
|
||||
* @param int $use_timestamp 1=>send request with ×tamp=1 to
|
||||
* get timestamp and session information
|
||||
* in the response
|
||||
* @param boolean $wait_for_all If true, wait until all
|
||||
* servers responds (for debugging)
|
||||
* @param string $sl Sync level in percentage between 0
|
||||
* and 100 or "fast" or "secure".
|
||||
* @param int $timeout Max number of seconds to wait
|
||||
* for responses
|
||||
* @param int $max_retries Max number of times we will retry on
|
||||
* transient errors.
|
||||
* @return mixed PEAR error on error, true otherwise
|
||||
* @access public
|
||||
*/
|
||||
function verify($token, $use_timestamp=null, $wait_for_all=False,
|
||||
$sl=null, $timeout=null, $max_retries=null)
|
||||
{
|
||||
/* If maximum retries is not set, default from instance */
|
||||
if (is_null($max_retries)) {
|
||||
$max_retries = $this->_max_retries;
|
||||
}
|
||||
|
||||
/* Construct parameters string */
|
||||
$ret = $this->parsePasswordOTP($token);
|
||||
if (!$ret) {
|
||||
return PEAR::raiseError('Could not parse Yubikey OTP');
|
||||
}
|
||||
$params = array('id'=>$this->_id,
|
||||
'otp'=>$ret['otp'],
|
||||
'nonce'=>md5(uniqid(rand())));
|
||||
/* Take care of protocol version 2 parameters */
|
||||
if ($use_timestamp) $params['timestamp'] = 1;
|
||||
if ($sl) $params['sl'] = $sl;
|
||||
if ($timeout) $params['timeout'] = $timeout;
|
||||
ksort($params);
|
||||
$parameters = '';
|
||||
foreach($params as $p=>$v) $parameters .= "&" . $p . "=" . $v;
|
||||
$parameters = ltrim($parameters, "&");
|
||||
|
||||
/* Generate signature. */
|
||||
if($this->_key <> "") {
|
||||
$signature = base64_encode(hash_hmac('sha1', $parameters,
|
||||
$this->_key, true));
|
||||
$signature = preg_replace('/\+/', '%2B', $signature);
|
||||
$parameters .= '&h=' . $signature;
|
||||
}
|
||||
|
||||
/* Generate and prepare request. */
|
||||
$this->_lastquery = null;
|
||||
$this->_retries = 0;
|
||||
$this->URLreset();
|
||||
|
||||
$mh = curl_multi_init();
|
||||
$ch = array();
|
||||
$retries = array();
|
||||
while($URLpart=$this->getNextURLpart())
|
||||
{
|
||||
$query = $URLpart . "?" . $parameters;
|
||||
|
||||
if ($this->_lastquery) { $this->_lastquery .= " "; }
|
||||
$this->_lastquery .= $query;
|
||||
|
||||
$handle = $this->_make_curl_handle($query, $timeout);
|
||||
curl_multi_add_handle($mh, $handle);
|
||||
|
||||
$ch[(int)$handle] = $handle;
|
||||
$retries[$query] = 0;
|
||||
}
|
||||
|
||||
/* Execute and read request. */
|
||||
$this->_response=null;
|
||||
$replay=False;
|
||||
$valid=False;
|
||||
do {
|
||||
/* Let curl do its work. */
|
||||
while (($mrc = curl_multi_exec($mh, $active))
|
||||
== CURLM_CALL_MULTI_PERFORM) {
|
||||
curl_multi_select($mh);
|
||||
}
|
||||
|
||||
while ($info = curl_multi_info_read($mh)) {
|
||||
$cinfo = curl_getinfo ($info['handle']);
|
||||
if ($info['result'] == CURLE_OK) {
|
||||
/* We have a complete response from one server. */
|
||||
|
||||
$str = curl_multi_getcontent($info['handle']);
|
||||
|
||||
if ($wait_for_all) { # Better debug info
|
||||
$this->_response .= 'URL=' . $cinfo['url'] . ' HTTP_CODE='
|
||||
. $cinfo['http_code'] . "\n"
|
||||
. $str . "\n";
|
||||
}
|
||||
|
||||
if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) {
|
||||
$status = $out[1];
|
||||
|
||||
/*
|
||||
* There are 3 cases.
|
||||
*
|
||||
* 1. OTP or Nonce values doesn't match - ignore
|
||||
* response.
|
||||
*
|
||||
* 2. We have a HMAC key. If signature is invalid -
|
||||
* ignore response. Return if status=OK or
|
||||
* status=REPLAYED_OTP.
|
||||
*
|
||||
* 3. Return if status=OK or status=REPLAYED_OTP.
|
||||
*/
|
||||
if (!preg_match("/otp=".$params['otp']."/", $str) ||
|
||||
!preg_match("/nonce=".$params['nonce']."/", $str)) {
|
||||
/* Case 1. Ignore response. */
|
||||
}
|
||||
elseif ($this->_key <> "") {
|
||||
/* Case 2. Verify signature first */
|
||||
$rows = explode("\r\n", trim($str));
|
||||
$response=array();
|
||||
foreach ($rows as $key => $val) {
|
||||
/* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */
|
||||
$val = preg_replace('/=/', '#', $val, 1);
|
||||
$row = explode("#", $val);
|
||||
$response[$row[0]] = $row[1];
|
||||
}
|
||||
|
||||
$parameters=array('nonce','otp', 'sessioncounter', 'sessionuse', 'sl', 'status', 't', 'timeout', 'timestamp');
|
||||
sort($parameters);
|
||||
$check=Null;
|
||||
foreach ($parameters as $param) {
|
||||
if (array_key_exists($param, $response)) {
|
||||
if ($check) $check = $check . '&';
|
||||
$check = $check . $param . '=' . $response[$param];
|
||||
}
|
||||
}
|
||||
|
||||
$checksignature =
|
||||
base64_encode(hash_hmac('sha1', utf8_encode($check),
|
||||
$this->_key, true));
|
||||
|
||||
if($response['h'] == $checksignature) {
|
||||
if ($status == 'REPLAYED_OTP') {
|
||||
if (!$wait_for_all) { $this->_response = $str; }
|
||||
$replay=True;
|
||||
}
|
||||
if ($status == 'OK') {
|
||||
if (!$wait_for_all) { $this->_response = $str; }
|
||||
$valid=True;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Case 3. We check the status directly */
|
||||
if ($status == 'REPLAYED_OTP') {
|
||||
if (!$wait_for_all) { $this->_response = $str; }
|
||||
$replay=True;
|
||||
}
|
||||
if ($status == 'OK') {
|
||||
if (!$wait_for_all) { $this->_response = $str; }
|
||||
$valid=True;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$wait_for_all && ($valid || $replay))
|
||||
{
|
||||
/* We have status=OK or status=REPLAYED_OTP, return. */
|
||||
foreach ($ch as $h) {
|
||||
curl_multi_remove_handle($mh, $h);
|
||||
curl_close($h);
|
||||
}
|
||||
curl_multi_close($mh);
|
||||
if ($replay) return PEAR::raiseError('REPLAYED_OTP');
|
||||
if ($valid) return true;
|
||||
return PEAR::raiseError($status);
|
||||
}
|
||||
} else {
|
||||
/* Some kind of error, but def. not a 200 response */
|
||||
/* No status= in response body */
|
||||
$http_status_code = (int)$cinfo['http_code'];
|
||||
$query = $cinfo['url'];
|
||||
if ($http_status_code == 400 ||
|
||||
($http_status_code >= 500 && $http_status_code < 600)) {
|
||||
/* maybe retry */
|
||||
if ($retries[$query] < $max_retries) {
|
||||
$retries[$query]++; // for this server
|
||||
$this->_retries++; // for this validation attempt
|
||||
|
||||
$newhandle = $this->_make_curl_handle($query, $timeout);
|
||||
|
||||
curl_multi_add_handle($mh, $newhandle);
|
||||
$ch[(int)$newhandle] = $newhandle;
|
||||
|
||||
// Loop back up to curl_multi_exec, even if this
|
||||
// was the last handle and curl_multi_exec _was_
|
||||
// no longer active, it's active again now we've
|
||||
// added a retry.
|
||||
$active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Done with this handle */
|
||||
curl_multi_remove_handle($mh, $info['handle']);
|
||||
curl_close($info['handle']);
|
||||
unset ($ch[(int)$info['handle']]);
|
||||
}
|
||||
} while ($active);
|
||||
|
||||
/* Typically this is only reached for wait_for_all=true or
|
||||
* when the timeout is reached and there is no
|
||||
* OK/REPLAYED_REQUEST answer (think firewall).
|
||||
*/
|
||||
|
||||
foreach ($ch as $h) {
|
||||
curl_multi_remove_handle ($mh, $h);
|
||||
curl_close ($h);
|
||||
}
|
||||
curl_multi_close ($mh);
|
||||
|
||||
if ($replay) return PEAR::raiseError('REPLAYED_OTP');
|
||||
if ($valid) return true;
|
||||
return PEAR::raiseError('NO_VALID_ANSWER');
|
||||
}
|
||||
}
|
||||
?>
|
||||
14
data/web/inc/lib/array_merge_real.php
Executable file
14
data/web/inc/lib/array_merge_real.php
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
function array_merge_real()
|
||||
{
|
||||
$output = [];
|
||||
foreach (func_get_args() as $array) {
|
||||
foreach ($array as $key => $value) {
|
||||
$output[$key] = isset($output[$key]) ?
|
||||
array_merge($output[$key], $value) : $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
15
data/web/inc/lib/composer.json
Executable file
15
data/web/inc/lib/composer.json
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"require": {
|
||||
"robthree/twofactorauth": "^1.6",
|
||||
"yubico/u2flib-server": "^1.0",
|
||||
"phpmailer/phpmailer": "^6.1",
|
||||
"php-mime-mail-parser/php-mime-mail-parser": "^7",
|
||||
"soundasleep/html2text": "^0.5.0",
|
||||
"ddeboer/imap": "^1.5",
|
||||
"matthiasmullie/minify": "^1.3",
|
||||
"bshaffer/oauth2-server-php": "^1.11",
|
||||
"mustangostang/spyc": "^0.6.3",
|
||||
"directorytree/ldaprecord": "^2.4",
|
||||
"twig/twig": "^3.0"
|
||||
}
|
||||
}
|
||||
1732
data/web/inc/lib/composer.lock
generated
Executable file
1732
data/web/inc/lib/composer.lock
generated
Executable file
File diff suppressed because it is too large
Load diff
7
data/web/inc/lib/sieve/SieveDumpable.php
Executable file
7
data/web/inc/lib/sieve/SieveDumpable.php
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
<?php namespace Sieve;
|
||||
|
||||
interface SieveDumpable
|
||||
{
|
||||
function dump();
|
||||
function text();
|
||||
}
|
||||
47
data/web/inc/lib/sieve/SieveException.php
Executable file
47
data/web/inc/lib/sieve/SieveException.php
Executable file
|
|
@ -0,0 +1,47 @@
|
|||
<?php namespace Sieve;
|
||||
|
||||
require_once('SieveToken.php');
|
||||
|
||||
use Exception;
|
||||
|
||||
class SieveException extends Exception
|
||||
{
|
||||
protected $token_;
|
||||
|
||||
public function __construct(SieveToken $token, $arg)
|
||||
{
|
||||
$message = 'undefined sieve exception';
|
||||
$this->token_ = $token;
|
||||
|
||||
if (is_string($arg))
|
||||
{
|
||||
$message = $arg;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (is_array($arg))
|
||||
{
|
||||
$type = SieveToken::typeString(array_shift($arg));
|
||||
foreach($arg as $t)
|
||||
{
|
||||
$type .= ' or '. SieveToken::typeString($t);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$type = SieveToken::typeString($arg);
|
||||
}
|
||||
|
||||
$tokenType = SieveToken::typeString($token->type);
|
||||
$message = "$tokenType where $type expected near ". $token->text;
|
||||
}
|
||||
|
||||
parent::__construct('line '. $token->line .": $message");
|
||||
}
|
||||
|
||||
public function getLineNo()
|
||||
{
|
||||
return $this->token_->line;
|
||||
}
|
||||
|
||||
}
|
||||
233
data/web/inc/lib/sieve/SieveKeywordRegistry.php
Executable file
233
data/web/inc/lib/sieve/SieveKeywordRegistry.php
Executable file
|
|
@ -0,0 +1,233 @@
|
|||
<?php namespace Sieve;
|
||||
|
||||
class SieveKeywordRegistry
|
||||
{
|
||||
protected $registry_ = array();
|
||||
protected $matchTypes_ = array();
|
||||
protected $comparators_ = array();
|
||||
protected $addressParts_ = array();
|
||||
protected $commands_ = array();
|
||||
protected $tests_ = array();
|
||||
protected $arguments_ = array();
|
||||
|
||||
protected static $refcount = 0;
|
||||
protected static $instance = null;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
$keywords = simplexml_load_file(dirname(__FILE__) .'/keywords.xml');
|
||||
foreach ($keywords->children() as $keyword)
|
||||
{
|
||||
switch ($keyword->getName())
|
||||
{
|
||||
case 'matchtype':
|
||||
$type =& $this->matchTypes_;
|
||||
break;
|
||||
case 'comparator':
|
||||
$type =& $this->comparators_;
|
||||
break;
|
||||
case 'addresspart':
|
||||
$type =& $this->addressParts_;
|
||||
break;
|
||||
case 'test':
|
||||
$type =& $this->tests_;
|
||||
break;
|
||||
case 'command':
|
||||
$type =& $this->commands_;
|
||||
break;
|
||||
default:
|
||||
trigger_error('Unsupported keyword type "'. $keyword->getName()
|
||||
. '" in file "keywords/'. basename($file) .'"');
|
||||
return;
|
||||
}
|
||||
|
||||
$name = (string) $keyword['name'];
|
||||
if (array_key_exists($name, $type))
|
||||
trigger_error("redefinition of $type $name - skipping");
|
||||
else
|
||||
$type[$name] = $keyword->children();
|
||||
}
|
||||
|
||||
foreach (glob(dirname(__FILE__) .'/extensions/*.xml') as $file)
|
||||
{
|
||||
$extension = simplexml_load_file($file);
|
||||
$name = (string) $extension['name'];
|
||||
|
||||
if (array_key_exists($name, $this->registry_))
|
||||
{
|
||||
trigger_error('overwriting extension "'. $name .'"');
|
||||
}
|
||||
$this->registry_[$name] = $extension;
|
||||
}
|
||||
}
|
||||
|
||||
public static function get()
|
||||
{
|
||||
if (self::$instance == null)
|
||||
{
|
||||
self::$instance = new SieveKeywordRegistry();
|
||||
}
|
||||
|
||||
self::$refcount++;
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function put()
|
||||
{
|
||||
if (--self::$refcount == 0)
|
||||
{
|
||||
self::$instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function activate($extension)
|
||||
{
|
||||
if (!isset($this->registry_[$extension]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$xml = $this->registry_[$extension];
|
||||
|
||||
foreach ($xml->children() as $e)
|
||||
{
|
||||
switch ($e->getName())
|
||||
{
|
||||
case 'matchtype':
|
||||
$type =& $this->matchTypes_;
|
||||
break;
|
||||
case 'comparator':
|
||||
$type =& $this->comparators_;
|
||||
break;
|
||||
case 'addresspart':
|
||||
$type =& $this->addressParts_;
|
||||
break;
|
||||
case 'test':
|
||||
$type =& $this->tests_;
|
||||
break;
|
||||
case 'command':
|
||||
$type =& $this->commands_;
|
||||
break;
|
||||
case 'tagged-argument':
|
||||
$xml = $e->parameter[0];
|
||||
$this->arguments_[(string) $xml['name']] = array(
|
||||
'extends' => (string) $e['extends'],
|
||||
'rules' => $xml
|
||||
);
|
||||
continue;
|
||||
default:
|
||||
trigger_error('Unsupported extension type \''.
|
||||
$e->getName() ."' in extension '$extension'");
|
||||
return;
|
||||
}
|
||||
|
||||
$name = (string) $e['name'];
|
||||
if (!isset($type[$name]) ||
|
||||
(string) $e['overrides'] == 'true')
|
||||
{
|
||||
$type[$name] = $e->children();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function isTest($name)
|
||||
{
|
||||
return (isset($this->tests_[$name]) ? true : false);
|
||||
}
|
||||
|
||||
public function isCommand($name)
|
||||
{
|
||||
return (isset($this->commands_[$name]) ? true : false);
|
||||
}
|
||||
|
||||
public function matchtype($name)
|
||||
{
|
||||
if (isset($this->matchTypes_[$name]))
|
||||
{
|
||||
return $this->matchTypes_[$name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function addresspart($name)
|
||||
{
|
||||
if (isset($this->addressParts_[$name]))
|
||||
{
|
||||
return $this->addressParts_[$name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function comparator($name)
|
||||
{
|
||||
if (isset($this->comparators_[$name]))
|
||||
{
|
||||
return $this->comparators_[$name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function test($name)
|
||||
{
|
||||
if (isset($this->tests_[$name]))
|
||||
{
|
||||
return $this->tests_[$name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function command($name)
|
||||
{
|
||||
if (isset($this->commands_[$name]))
|
||||
{
|
||||
return $this->commands_[$name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function arguments($command)
|
||||
{
|
||||
$res = array();
|
||||
foreach ($this->arguments_ as $arg)
|
||||
{
|
||||
if (preg_match('/'.$arg['extends'].'/', $command))
|
||||
array_push($res, $arg['rules']);
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function argument($name)
|
||||
{
|
||||
if (isset($this->arguments_[$name]))
|
||||
{
|
||||
return $this->arguments_[$name]['rules'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function requireStrings()
|
||||
{
|
||||
return array_keys($this->registry_);
|
||||
}
|
||||
public function matchTypes()
|
||||
{
|
||||
return array_keys($this->matchTypes_);
|
||||
}
|
||||
public function comparators()
|
||||
{
|
||||
return array_keys($this->comparators_);
|
||||
}
|
||||
public function addressParts()
|
||||
{
|
||||
return array_keys($this->addressParts_);
|
||||
}
|
||||
public function tests()
|
||||
{
|
||||
return array_keys($this->tests_);
|
||||
}
|
||||
public function commands()
|
||||
{
|
||||
return array_keys($this->commands_);
|
||||
}
|
||||
}
|
||||
255
data/web/inc/lib/sieve/SieveParser.php
Executable file
255
data/web/inc/lib/sieve/SieveParser.php
Executable file
|
|
@ -0,0 +1,255 @@
|
|||
<?php namespace Sieve;
|
||||
|
||||
include_once 'SieveTree.php';
|
||||
include_once 'SieveScanner.php';
|
||||
include_once 'SieveSemantics.php';
|
||||
include_once 'SieveException.php';
|
||||
|
||||
class SieveParser
|
||||
{
|
||||
protected $scanner_;
|
||||
protected $script_;
|
||||
protected $tree_;
|
||||
protected $status_;
|
||||
|
||||
public function __construct($script = null)
|
||||
{
|
||||
if (isset($script))
|
||||
$this->parse($script);
|
||||
}
|
||||
|
||||
public function GetParseTree()
|
||||
{
|
||||
return $this->tree_;
|
||||
}
|
||||
|
||||
public function dumpParseTree()
|
||||
{
|
||||
return $this->tree_->dump();
|
||||
}
|
||||
|
||||
public function getScriptText()
|
||||
{
|
||||
return $this->tree_->getText();
|
||||
}
|
||||
|
||||
protected function getPrevToken_($parent_id)
|
||||
{
|
||||
$childs = $this->tree_->getChilds($parent_id);
|
||||
|
||||
for ($i = count($childs); $i > 0; --$i)
|
||||
{
|
||||
$prev = $this->tree_->getNode($childs[$i-1]);
|
||||
if ($prev->is(SieveToken::Comment|SieveToken::Whitespace))
|
||||
continue;
|
||||
|
||||
// use command owning a block or list instead of previous
|
||||
if ($prev->is(SieveToken::BlockStart|SieveToken::Comma|SieveToken::LeftParenthesis))
|
||||
$prev = $this->tree_->getNode($parent_id);
|
||||
|
||||
return $prev;
|
||||
}
|
||||
|
||||
return $this->tree_->getNode($parent_id);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* methods for recursive descent start below
|
||||
*/
|
||||
public function passthroughWhitespaceComment($token)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function passthroughFunction($token)
|
||||
{
|
||||
$this->tree_->addChild($token);
|
||||
}
|
||||
|
||||
public function parse($script)
|
||||
{
|
||||
$this->script_ = $script;
|
||||
|
||||
$this->scanner_ = new SieveScanner($this->script_);
|
||||
|
||||
// Define what happens with passthrough tokens like whitespacs and comments
|
||||
$this->scanner_->setPassthroughFunc(
|
||||
array(
|
||||
$this, 'passthroughWhitespaceComment'
|
||||
)
|
||||
);
|
||||
|
||||
$this->tree_ = new SieveTree('tree');
|
||||
|
||||
$this->commands_($this->tree_->getRoot());
|
||||
|
||||
if (!$this->scanner_->nextTokenIs(SieveToken::ScriptEnd)) {
|
||||
$token = $this->scanner_->nextToken();
|
||||
throw new SieveException($token, SieveToken::ScriptEnd);
|
||||
}
|
||||
}
|
||||
|
||||
protected function commands_($parent_id)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (!$this->scanner_->nextTokenIs(SieveToken::Identifier))
|
||||
break;
|
||||
|
||||
// Get and check a command token
|
||||
$token = $this->scanner_->nextToken();
|
||||
$semantics = new SieveSemantics($token, $this->getPrevToken_($parent_id));
|
||||
|
||||
// Process eventual arguments
|
||||
$this_node = $this->tree_->addChildTo($parent_id, $token);
|
||||
$this->arguments_($this_node, $semantics);
|
||||
|
||||
$token = $this->scanner_->nextToken();
|
||||
if (!$token->is(SieveToken::Semicolon))
|
||||
{
|
||||
// TODO: check if/when semcheck is needed here
|
||||
$semantics->validateToken($token);
|
||||
|
||||
if ($token->is(SieveToken::BlockStart))
|
||||
{
|
||||
$this->tree_->addChildTo($this_node, $token);
|
||||
$this->block_($this_node, $semantics);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new SieveException($token, SieveToken::Semicolon);
|
||||
}
|
||||
|
||||
$semantics->done($token);
|
||||
$this->tree_->addChildTo($this_node, $token);
|
||||
}
|
||||
}
|
||||
|
||||
protected function arguments_($parent_id, &$semantics)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if ($this->scanner_->nextTokenIs(SieveToken::Number|SieveToken::Tag))
|
||||
{
|
||||
// Check if semantics allow a number or tag
|
||||
$token = $this->scanner_->nextToken();
|
||||
$semantics->validateToken($token);
|
||||
$this->tree_->addChildTo($parent_id, $token);
|
||||
}
|
||||
else if ($this->scanner_->nextTokenIs(SieveToken::StringList))
|
||||
{
|
||||
$this->stringlist_($parent_id, $semantics);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->scanner_->nextTokenIs(SieveToken::TestList))
|
||||
{
|
||||
$this->testlist_($parent_id, $semantics);
|
||||
}
|
||||
}
|
||||
|
||||
protected function stringlist_($parent_id, &$semantics)
|
||||
{
|
||||
if (!$this->scanner_->nextTokenIs(SieveToken::LeftBracket))
|
||||
{
|
||||
$this->string_($parent_id, $semantics);
|
||||
return;
|
||||
}
|
||||
|
||||
$token = $this->scanner_->nextToken();
|
||||
$semantics->startStringList($token);
|
||||
$this->tree_->addChildTo($parent_id, $token);
|
||||
|
||||
if($this->scanner_->nextTokenIs(SieveToken::RightBracket)) {
|
||||
//allow empty lists
|
||||
$token = $this->scanner_->nextToken();
|
||||
$this->tree_->addChildTo($parent_id, $token);
|
||||
$semantics->endStringList();
|
||||
return;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
$this->string_($parent_id, $semantics);
|
||||
$token = $this->scanner_->nextToken();
|
||||
|
||||
if (!$token->is(SieveToken::Comma|SieveToken::RightBracket))
|
||||
throw new SieveException($token, array(SieveToken::Comma, SieveToken::RightBracket));
|
||||
|
||||
if ($token->is(SieveToken::Comma))
|
||||
$semantics->continueStringList();
|
||||
|
||||
$this->tree_->addChildTo($parent_id, $token);
|
||||
}
|
||||
while (!$token->is(SieveToken::RightBracket));
|
||||
|
||||
$semantics->endStringList();
|
||||
}
|
||||
|
||||
protected function string_($parent_id, &$semantics)
|
||||
{
|
||||
$token = $this->scanner_->nextToken();
|
||||
$semantics->validateToken($token);
|
||||
$this->tree_->addChildTo($parent_id, $token);
|
||||
}
|
||||
|
||||
protected function testlist_($parent_id, &$semantics)
|
||||
{
|
||||
if (!$this->scanner_->nextTokenIs(SieveToken::LeftParenthesis))
|
||||
{
|
||||
$this->test_($parent_id, $semantics);
|
||||
return;
|
||||
}
|
||||
|
||||
$token = $this->scanner_->nextToken();
|
||||
$semantics->validateToken($token);
|
||||
$this->tree_->addChildTo($parent_id, $token);
|
||||
|
||||
do
|
||||
{
|
||||
$this->test_($parent_id, $semantics);
|
||||
|
||||
$token = $this->scanner_->nextToken();
|
||||
if (!$token->is(SieveToken::Comma|SieveToken::RightParenthesis))
|
||||
{
|
||||
throw new SieveException($token, array(SieveToken::Comma, SieveToken::RightParenthesis));
|
||||
}
|
||||
$this->tree_->addChildTo($parent_id, $token);
|
||||
}
|
||||
while (!$token->is(SieveToken::RightParenthesis));
|
||||
}
|
||||
|
||||
protected function test_($parent_id, &$semantics)
|
||||
{
|
||||
// Check if semantics allow an identifier
|
||||
$token = $this->scanner_->nextToken();
|
||||
$semantics->validateToken($token);
|
||||
|
||||
// Get semantics for this test command
|
||||
$this_semantics = new SieveSemantics($token, $this->getPrevToken_($parent_id));
|
||||
$this_node = $this->tree_->addChildTo($parent_id, $token);
|
||||
|
||||
// Consume eventual argument tokens
|
||||
$this->arguments_($this_node, $this_semantics);
|
||||
|
||||
// Check that all required arguments were there
|
||||
$token = $this->scanner_->peekNextToken();
|
||||
$this_semantics->done($token);
|
||||
}
|
||||
|
||||
protected function block_($parent_id, &$semantics)
|
||||
{
|
||||
$this->commands_($parent_id, $semantics);
|
||||
|
||||
$token = $this->scanner_->nextToken();
|
||||
if (!$token->is(SieveToken::BlockEnd))
|
||||
{
|
||||
throw new SieveException($token, SieveToken::BlockEnd);
|
||||
}
|
||||
$this->tree_->addChildTo($parent_id, $token);
|
||||
}
|
||||
}
|
||||
145
data/web/inc/lib/sieve/SieveScanner.php
Executable file
145
data/web/inc/lib/sieve/SieveScanner.php
Executable file
|
|
@ -0,0 +1,145 @@
|
|||
<?php namespace Sieve;
|
||||
|
||||
include_once('SieveToken.php');
|
||||
|
||||
class SieveScanner
|
||||
{
|
||||
public function __construct(&$script)
|
||||
{
|
||||
if ($script === null)
|
||||
return;
|
||||
|
||||
$this->tokenize($script);
|
||||
}
|
||||
|
||||
public function setPassthroughFunc($callback)
|
||||
{
|
||||
if ($callback == null || is_callable($callback))
|
||||
$this->ptFn_ = $callback;
|
||||
}
|
||||
|
||||
public function tokenize(&$script)
|
||||
{
|
||||
$pos = 0;
|
||||
$line = 1;
|
||||
|
||||
$scriptLength = mb_strlen($script);
|
||||
|
||||
$unprocessedScript = $script;
|
||||
|
||||
|
||||
//create one regex to find the right match
|
||||
//avoids looping over all possible tokens: increases performance
|
||||
$nameToType = [];
|
||||
$regex = [];
|
||||
// chr(65) == 'A'
|
||||
$i = 65;
|
||||
|
||||
foreach ($this->tokenMatch_ as $type => $subregex) {
|
||||
$nameToType[chr($i)] = $type;
|
||||
$regex[] = "(?P<". chr($i) . ">^$subregex)";
|
||||
$i++;
|
||||
}
|
||||
|
||||
$regex = '/' . join('|', $regex) . '/';
|
||||
|
||||
while ($pos < $scriptLength)
|
||||
{
|
||||
if (preg_match($regex, $unprocessedScript, $match)) {
|
||||
|
||||
// only keep the group that match and we only want matches with group names
|
||||
// we can use the group name to find the token type using nameToType
|
||||
$filterMatch = array_filter(array_filter($match), 'is_string', ARRAY_FILTER_USE_KEY);
|
||||
|
||||
// the first element in filterMatch will contain the matched group and the key will be the name
|
||||
$type = $nameToType[key($filterMatch)];
|
||||
$currentMatch = current($filterMatch);
|
||||
|
||||
//create the token
|
||||
$token = new SieveToken($type, $currentMatch, $line);
|
||||
$this->tokens_[] = $token;
|
||||
|
||||
if ($type == SieveToken::Unknown)
|
||||
return;
|
||||
|
||||
// just remove the part that we parsed: don't extract the new substring using script length
|
||||
// as mb_strlen is \theta(pos) (it's linear in the position)
|
||||
$matchLength = mb_strlen($currentMatch);
|
||||
$unprocessedScript = mb_substr($unprocessedScript, $matchLength);
|
||||
|
||||
$pos += $matchLength;
|
||||
$line += mb_substr_count($currentMatch, "\n");
|
||||
} else {
|
||||
$this->tokens_[] = new SieveToken(SieveToken::Unknown, '', $line);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->tokens_[] = new SieveToken(SieveToken::ScriptEnd, '', $line);
|
||||
}
|
||||
|
||||
public function nextTokenIs($type)
|
||||
{
|
||||
return $this->peekNextToken()->is($type);
|
||||
}
|
||||
|
||||
public function peekNextToken()
|
||||
{
|
||||
$offset = 0;
|
||||
do {
|
||||
$next = $this->tokens_[$this->tokenPos_ + $offset++];
|
||||
} while ($next->is(SieveToken::Comment|SieveToken::Whitespace));
|
||||
|
||||
return $next;
|
||||
}
|
||||
|
||||
public function nextToken()
|
||||
{
|
||||
$token = $this->tokens_[$this->tokenPos_++];
|
||||
|
||||
while ($token->is(SieveToken::Comment|SieveToken::Whitespace))
|
||||
{
|
||||
if ($this->ptFn_ != null)
|
||||
call_user_func($this->ptFn_, $token);
|
||||
|
||||
$token = $this->tokens_[$this->tokenPos_++];
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
protected $ptFn_ = null;
|
||||
protected $tokenPos_ = 0;
|
||||
protected $tokens_ = array();
|
||||
protected $tokenMatch_ = array (
|
||||
SieveToken::LeftBracket => '\[',
|
||||
SieveToken::RightBracket => '\]',
|
||||
SieveToken::BlockStart => '\{',
|
||||
SieveToken::BlockEnd => '\}',
|
||||
SieveToken::LeftParenthesis => '\(',
|
||||
SieveToken::RightParenthesis => '\)',
|
||||
SieveToken::Comma => ',',
|
||||
SieveToken::Semicolon => ';',
|
||||
SieveToken::Whitespace => '[ \r\n\t]+',
|
||||
SieveToken::Tag => ':[[:alpha:]_][[:alnum:]_]*(?=\b)',
|
||||
/*
|
||||
" # match a quotation mark
|
||||
( # start matching parts that include an escaped quotation mark
|
||||
([^"]*[^"\\\\]) # match a string without quotation marks and not ending with a backlash
|
||||
? # this also includes the empty string
|
||||
(\\\\\\\\)* # match any groups of even number of backslashes
|
||||
# (thus the character after these groups are not escaped)
|
||||
\\\\" # match an escaped quotation mark
|
||||
)* # accept any number of strings that end with an escaped quotation mark
|
||||
[^"]* # accept any trailing part that does not contain any quotation marks
|
||||
" # end of the quoted string
|
||||
*/
|
||||
SieveToken::QuotedString => '"(([^"]*[^"\\\\])?(\\\\\\\\)*\\\\")*[^"]*"',
|
||||
SieveToken::Number => '[[:digit:]]+(?:[KMG])?(?=\b)',
|
||||
SieveToken::Comment => '(?:\/\*(?:[^\*]|\*(?=[^\/]))*\*\/|#[^\r\n]*\r?(\n|$))',
|
||||
SieveToken::MultilineString => 'text:[ \t]*(?:#[^\r\n]*)?\r?\n(\.[^\r\n]+\r?\n|[^\.][^\r\n]*\r?\n)*\.\r?(\n|$)',
|
||||
SieveToken::Identifier => '[[:alpha:]_][[:alnum:]_]*(?=\b)',
|
||||
SieveToken::Unknown => '[^ \r\n\t]+'
|
||||
);
|
||||
}
|
||||
6
data/web/inc/lib/sieve/SieveScript.php
Executable file
6
data/web/inc/lib/sieve/SieveScript.php
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
<?php namespace Sieve;
|
||||
|
||||
class SieveScript
|
||||
{
|
||||
// TODO: implement
|
||||
}
|
||||
611
data/web/inc/lib/sieve/SieveSemantics.php
Executable file
611
data/web/inc/lib/sieve/SieveSemantics.php
Executable file
|
|
@ -0,0 +1,611 @@
|
|||
<?php namespace Sieve;
|
||||
|
||||
require_once('SieveKeywordRegistry.php');
|
||||
require_once('SieveToken.php');
|
||||
require_once('SieveException.php');
|
||||
|
||||
class SieveSemantics
|
||||
{
|
||||
protected static $requiredExtensions_ = array();
|
||||
|
||||
protected $comparator_;
|
||||
protected $matchType_;
|
||||
protected $addressPart_;
|
||||
protected $tags_ = array();
|
||||
protected $arguments_;
|
||||
protected $deps_ = array();
|
||||
protected $followupToken_;
|
||||
|
||||
public function __construct($token, $prevToken)
|
||||
{
|
||||
$this->registry_ = SieveKeywordRegistry::get();
|
||||
$command = strtolower($token->text);
|
||||
|
||||
// Check the registry for $command
|
||||
if ($this->registry_->isCommand($command))
|
||||
{
|
||||
$xml = $this->registry_->command($command);
|
||||
$this->arguments_ = $this->makeArguments_($xml);
|
||||
$this->followupToken_ = SieveToken::Semicolon;
|
||||
}
|
||||
else if ($this->registry_->isTest($command))
|
||||
{
|
||||
$xml = $this->registry_->test($command);
|
||||
$this->arguments_ = $this->makeArguments_($xml);
|
||||
$this->followupToken_ = SieveToken::BlockStart;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new SieveException($token, 'unknown command '. $command);
|
||||
}
|
||||
|
||||
// Check if command may appear at this position within the script
|
||||
if ($this->registry_->isTest($command))
|
||||
{
|
||||
if (is_null($prevToken))
|
||||
throw new SieveException($token, $command .' may not appear as first command');
|
||||
|
||||
if (!preg_match('/^(if|elsif|anyof|allof|not)$/i', $prevToken->text))
|
||||
throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
|
||||
}
|
||||
else if (isset($prevToken))
|
||||
{
|
||||
switch ($command)
|
||||
{
|
||||
case 'require':
|
||||
$valid_after = 'require';
|
||||
break;
|
||||
case 'elsif':
|
||||
case 'else':
|
||||
$valid_after = '(if|elsif)';
|
||||
break;
|
||||
default:
|
||||
$valid_after = $this->commandsRegex_();
|
||||
}
|
||||
|
||||
if (!preg_match('/^'. $valid_after .'$/i', $prevToken->text))
|
||||
throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
|
||||
}
|
||||
|
||||
// Check for extension arguments to add to the command
|
||||
foreach ($this->registry_->arguments($command) as $arg)
|
||||
{
|
||||
switch ((string) $arg['type'])
|
||||
{
|
||||
case 'tag':
|
||||
array_unshift($this->arguments_, array(
|
||||
'type' => SieveToken::Tag,
|
||||
'occurrence' => $this->occurrence_($arg),
|
||||
'regex' => $this->regex_($arg),
|
||||
'call' => 'tagHook_',
|
||||
'name' => $this->name_($arg),
|
||||
'subArgs' => $this->makeArguments_($arg->children())
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->registry_->put();
|
||||
}
|
||||
|
||||
// TODO: the *Regex functions could possibly also be static properties
|
||||
protected function requireStringsRegex_()
|
||||
{
|
||||
return '('. implode('|', $this->registry_->requireStrings()) .')';
|
||||
}
|
||||
|
||||
protected function matchTypeRegex_()
|
||||
{
|
||||
return '('. implode('|', $this->registry_->matchTypes()) .')';
|
||||
}
|
||||
|
||||
protected function addressPartRegex_()
|
||||
{
|
||||
return '('. implode('|', $this->registry_->addressParts()) .')';
|
||||
}
|
||||
|
||||
protected function commandsRegex_()
|
||||
{
|
||||
return '('. implode('|', $this->registry_->commands()) .')';
|
||||
}
|
||||
|
||||
protected function testsRegex_()
|
||||
{
|
||||
return '('. implode('|', $this->registry_->tests()) .')';
|
||||
}
|
||||
|
||||
protected function comparatorRegex_()
|
||||
{
|
||||
return '('. implode('|', $this->registry_->comparators()) .')';
|
||||
}
|
||||
|
||||
protected function occurrence_($arg)
|
||||
{
|
||||
if (isset($arg['occurrence']))
|
||||
{
|
||||
switch ((string) $arg['occurrence'])
|
||||
{
|
||||
case 'optional':
|
||||
return '?';
|
||||
case 'any':
|
||||
return '*';
|
||||
case 'some':
|
||||
return '+';
|
||||
}
|
||||
}
|
||||
return '1';
|
||||
}
|
||||
|
||||
protected function name_($arg)
|
||||
{
|
||||
if (isset($arg['name']))
|
||||
{
|
||||
return (string) $arg['name'];
|
||||
}
|
||||
return (string) $arg['type'];
|
||||
}
|
||||
|
||||
protected function regex_($arg)
|
||||
{
|
||||
if (isset($arg['regex']))
|
||||
{
|
||||
return (string) $arg['regex'];
|
||||
}
|
||||
return '.*';
|
||||
}
|
||||
|
||||
protected function case_($arg)
|
||||
{
|
||||
if (isset($arg['case']))
|
||||
{
|
||||
return (string) $arg['case'];
|
||||
}
|
||||
return 'adhere';
|
||||
}
|
||||
|
||||
protected function follows_($arg)
|
||||
{
|
||||
if (isset($arg['follows']))
|
||||
{
|
||||
return (string) $arg['follows'];
|
||||
}
|
||||
return '.*';
|
||||
}
|
||||
|
||||
protected function makeValue_($arg)
|
||||
{
|
||||
if (isset($arg->value))
|
||||
{
|
||||
$res = $this->makeArguments_($arg->value);
|
||||
return array_shift($res);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an extension (test) commands parameters from XML to
|
||||
* a PHP array the {@see Semantics} class understands.
|
||||
* @param array(SimpleXMLElement) $parameters
|
||||
* @return array
|
||||
*/
|
||||
protected function makeArguments_($parameters)
|
||||
{
|
||||
$arguments = array();
|
||||
|
||||
foreach ($parameters as $arg)
|
||||
{
|
||||
// Ignore anything not a <parameter>
|
||||
if ($arg->getName() != 'parameter')
|
||||
continue;
|
||||
|
||||
switch ((string) $arg['type'])
|
||||
{
|
||||
case 'addresspart':
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::Tag,
|
||||
'occurrence' => $this->occurrence_($arg),
|
||||
'regex' => $this->addressPartRegex_(),
|
||||
'call' => 'addressPartHook_',
|
||||
'name' => 'address part',
|
||||
'subArgs' => $this->makeArguments_($arg)
|
||||
));
|
||||
break;
|
||||
|
||||
case 'block':
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::BlockStart,
|
||||
'occurrence' => '1',
|
||||
'regex' => '{',
|
||||
'name' => 'block',
|
||||
'subArgs' => $this->makeArguments_($arg)
|
||||
));
|
||||
break;
|
||||
|
||||
case 'comparator':
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::Tag,
|
||||
'occurrence' => $this->occurrence_($arg),
|
||||
'regex' => 'comparator',
|
||||
'name' => 'comparator',
|
||||
'subArgs' => array( array(
|
||||
'type' => SieveToken::String,
|
||||
'occurrence' => '1',
|
||||
'call' => 'comparatorHook_',
|
||||
'case' => 'adhere',
|
||||
'regex' => $this->comparatorRegex_(),
|
||||
'name' => 'comparator string',
|
||||
'follows' => 'comparator'
|
||||
))
|
||||
));
|
||||
break;
|
||||
|
||||
case 'matchtype':
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::Tag,
|
||||
'occurrence' => $this->occurrence_($arg),
|
||||
'regex' => $this->matchTypeRegex_(),
|
||||
'call' => 'matchTypeHook_',
|
||||
'name' => 'match type',
|
||||
'subArgs' => $this->makeArguments_($arg)
|
||||
));
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::Number,
|
||||
'occurrence' => $this->occurrence_($arg),
|
||||
'regex' => $this->regex_($arg),
|
||||
'name' => $this->name_($arg),
|
||||
'follows' => $this->follows_($arg)
|
||||
));
|
||||
break;
|
||||
|
||||
case 'requirestrings':
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::StringList,
|
||||
'occurrence' => $this->occurrence_($arg),
|
||||
'call' => 'setRequire_',
|
||||
'case' => 'adhere',
|
||||
'regex' => $this->requireStringsRegex_(),
|
||||
'name' => $this->name_($arg)
|
||||
));
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::String,
|
||||
'occurrence' => $this->occurrence_($arg),
|
||||
'regex' => $this->regex_($arg),
|
||||
'case' => $this->case_($arg),
|
||||
'name' => $this->name_($arg),
|
||||
'follows' => $this->follows_($arg)
|
||||
));
|
||||
break;
|
||||
|
||||
case 'stringlist':
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::StringList,
|
||||
'occurrence' => $this->occurrence_($arg),
|
||||
'regex' => $this->regex_($arg),
|
||||
'case' => $this->case_($arg),
|
||||
'name' => $this->name_($arg),
|
||||
'follows' => $this->follows_($arg)
|
||||
));
|
||||
break;
|
||||
|
||||
case 'tag':
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::Tag,
|
||||
'occurrence' => $this->occurrence_($arg),
|
||||
'regex' => $this->regex_($arg),
|
||||
'call' => 'tagHook_',
|
||||
'name' => $this->name_($arg),
|
||||
'subArgs' => $this->makeArguments_($arg->children()),
|
||||
'follows' => $this->follows_($arg)
|
||||
));
|
||||
break;
|
||||
|
||||
case 'test':
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::Identifier,
|
||||
'occurrence' => $this->occurrence_($arg),
|
||||
'regex' => $this->testsRegex_(),
|
||||
'name' => $this->name_($arg),
|
||||
'subArgs' => $this->makeArguments_($arg->children())
|
||||
));
|
||||
break;
|
||||
|
||||
case 'testlist':
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::LeftParenthesis,
|
||||
'occurrence' => '1',
|
||||
'regex' => '\(',
|
||||
'name' => $this->name_($arg),
|
||||
'subArgs' => null
|
||||
));
|
||||
array_push($arguments, array(
|
||||
'type' => SieveToken::Identifier,
|
||||
'occurrence' => '+',
|
||||
'regex' => $this->testsRegex_(),
|
||||
'name' => $this->name_($arg),
|
||||
'subArgs' => $this->makeArguments_($arg->children())
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add argument(s) expected / allowed to appear next.
|
||||
* @param array $value
|
||||
*/
|
||||
protected function addArguments_($identifier, $subArgs)
|
||||
{
|
||||
for ($i = count($subArgs); $i > 0; $i--)
|
||||
{
|
||||
$arg = $subArgs[$i-1];
|
||||
if (preg_match('/^'. $arg['follows'] .'$/si', $identifier))
|
||||
array_unshift($this->arguments_, $arg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add dependency that is expected to be fullfilled when parsing
|
||||
* of the current command is {@see done}.
|
||||
* @param array $dependency
|
||||
*/
|
||||
protected function addDependency_($type, $name, $dependencies)
|
||||
{
|
||||
foreach ($dependencies as $d)
|
||||
{
|
||||
array_push($this->deps_, array(
|
||||
'o_type' => $type,
|
||||
'o_name' => $name,
|
||||
'type' => $d['type'],
|
||||
'name' => $d['name'],
|
||||
'regex' => $d['regex']
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
protected function invoke_($token, $func, $arg = array())
|
||||
{
|
||||
if (!is_array($arg))
|
||||
$arg = array($arg);
|
||||
|
||||
$err = call_user_func_array(array(&$this, $func), $arg);
|
||||
|
||||
if ($err)
|
||||
throw new SieveException($token, $err);
|
||||
}
|
||||
|
||||
protected function setRequire_($extension)
|
||||
{
|
||||
array_push(self::$requiredExtensions_, $extension);
|
||||
$this->registry_->activate($extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook function that is called after a address part match was found
|
||||
* in a command. The kind of address part is remembered in case it's
|
||||
* needed later {@see done}. For address parts from a extension
|
||||
* dependency information and valid values are looked up as well.
|
||||
* @param string $addresspart
|
||||
*/
|
||||
protected function addressPartHook_($addresspart)
|
||||
{
|
||||
$this->addressPart_ = $addresspart;
|
||||
$xml = $this->registry_->addresspart($this->addressPart_);
|
||||
|
||||
if (isset($xml))
|
||||
{
|
||||
// Add possible value and dependancy
|
||||
$this->addArguments_($this->addressPart_, $this->makeArguments_($xml));
|
||||
$this->addDependency_('address part', $this->addressPart_, $xml->requires);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook function that is called after a match type was found in a
|
||||
* command. The kind of match type is remembered in case it's
|
||||
* needed later {@see done}. For a match type from extensions
|
||||
* dependency information and valid values are looked up as well.
|
||||
* @param string $matchtype
|
||||
*/
|
||||
protected function matchTypeHook_($matchtype)
|
||||
{
|
||||
$this->matchType_ = $matchtype;
|
||||
$xml = $this->registry_->matchtype($this->matchType_);
|
||||
|
||||
if (isset($xml))
|
||||
{
|
||||
// Add possible value and dependancy
|
||||
$this->addArguments_($this->matchType_, $this->makeArguments_($xml));
|
||||
$this->addDependency_('match type', $this->matchType_, $xml->requires);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook function that is called after a comparator was found in
|
||||
* a command. The comparator is remembered in case it's needed for
|
||||
* comparsion later {@see done}. For a comparator from extensions
|
||||
* dependency information is looked up as well.
|
||||
* @param string $comparator
|
||||
*/
|
||||
protected function comparatorHook_($comparator)
|
||||
{
|
||||
$this->comparator_ = $comparator;
|
||||
$xml = $this->registry_->comparator($this->comparator_);
|
||||
|
||||
if (isset($xml))
|
||||
{
|
||||
// Add possible dependancy
|
||||
$this->addDependency_('comparator', $this->comparator_, $xml->requires);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook function that is called after a tag was found in
|
||||
* a command. The tag is remembered in case it's needed for
|
||||
* comparsion later {@see done}. For a tags from extensions
|
||||
* dependency information is looked up as well.
|
||||
* @param string $tag
|
||||
*/
|
||||
protected function tagHook_($tag)
|
||||
{
|
||||
array_push($this->tags_, $tag);
|
||||
$xml = $this->registry_->argument($tag);
|
||||
|
||||
// Add possible dependancies
|
||||
if (isset($xml))
|
||||
$this->addDependency_('tag', $tag, $xml->requires);
|
||||
}
|
||||
|
||||
protected function validType_($token)
|
||||
{
|
||||
foreach ($this->arguments_ as $arg)
|
||||
{
|
||||
if ($arg['occurrence'] == '0')
|
||||
{
|
||||
array_shift($this->arguments_);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($token->is($arg['type']))
|
||||
return;
|
||||
|
||||
// Is the argument required
|
||||
if ($arg['occurrence'] != '?' && $arg['occurrence'] != '*')
|
||||
throw new SieveException($token, $arg['type']);
|
||||
|
||||
array_shift($this->arguments_);
|
||||
}
|
||||
|
||||
// Check if command expects any (more) arguments
|
||||
if (empty($this->arguments_))
|
||||
throw new SieveException($token, $this->followupToken_);
|
||||
|
||||
throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
|
||||
}
|
||||
|
||||
public function startStringList($token)
|
||||
{
|
||||
$this->validType_($token);
|
||||
$this->arguments_[0]['type'] = SieveToken::String;
|
||||
$this->arguments_[0]['occurrence'] = '+';
|
||||
}
|
||||
|
||||
public function continueStringList()
|
||||
{
|
||||
$this->arguments_[0]['occurrence'] = '+';
|
||||
}
|
||||
|
||||
public function endStringList()
|
||||
{
|
||||
array_shift($this->arguments_);
|
||||
}
|
||||
|
||||
public function validateToken($token)
|
||||
{
|
||||
// Make sure the argument has a valid type
|
||||
$this->validType_($token);
|
||||
|
||||
foreach ($this->arguments_ as &$arg)
|
||||
{
|
||||
// Build regular expression according to argument type
|
||||
switch ($arg['type'])
|
||||
{
|
||||
case SieveToken::String:
|
||||
case SieveToken::StringList:
|
||||
$regex = '/^(?:text:[^\n]*\n(?P<one>'. $arg['regex'] .')\.\r?\n?|"(?P<two>'. $arg['regex'] .')")$/'
|
||||
. ($arg['case'] == 'ignore' ? 'si' : 's');
|
||||
break;
|
||||
case SieveToken::Tag:
|
||||
$regex = '/^:(?P<one>'. $arg['regex'] .')$/si';
|
||||
break;
|
||||
default:
|
||||
$regex = '/^(?P<one>'. $arg['regex'] .')$/si';
|
||||
}
|
||||
|
||||
if (preg_match($regex, $token->text, $match))
|
||||
{
|
||||
$text = ($match['one'] ? $match['one'] : $match['two']);
|
||||
|
||||
// Add argument(s) that may now appear after this one
|
||||
if (isset($arg['subArgs']))
|
||||
$this->addArguments_($text, $arg['subArgs']);
|
||||
|
||||
// Call extra processing function if defined
|
||||
if (isset($arg['call']))
|
||||
$this->invoke_($token, $arg['call'], $text);
|
||||
|
||||
// Check if a possible value of this argument may occur
|
||||
if ($arg['occurrence'] == '?' || $arg['occurrence'] == '1')
|
||||
{
|
||||
$arg['occurrence'] = '0';
|
||||
}
|
||||
else if ($arg['occurrence'] == '+')
|
||||
{
|
||||
$arg['occurrence'] = '*';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($token->is($arg['type']) && $arg['occurrence'] == 1)
|
||||
{
|
||||
throw new SieveException($token,
|
||||
SieveToken::typeString($token->type) ." $token->text where ". $arg['name'] .' expected');
|
||||
}
|
||||
}
|
||||
|
||||
throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
|
||||
}
|
||||
|
||||
public function done($token)
|
||||
{
|
||||
// Check if there are required arguments left
|
||||
foreach ($this->arguments_ as $arg)
|
||||
{
|
||||
if ($arg['occurrence'] == '+' || $arg['occurrence'] == '1')
|
||||
throw new SieveException($token, $arg['type']);
|
||||
}
|
||||
|
||||
// Check if the command depends on use of a certain tag
|
||||
foreach ($this->deps_ as $d)
|
||||
{
|
||||
switch ($d['type'])
|
||||
{
|
||||
case 'addresspart':
|
||||
$values = array($this->addressPart_);
|
||||
break;
|
||||
|
||||
case 'matchtype':
|
||||
$values = array($this->matchType_);
|
||||
break;
|
||||
|
||||
case 'comparator':
|
||||
$values = array($this->comparator_);
|
||||
break;
|
||||
|
||||
case 'tag':
|
||||
$values = $this->tags_;
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($values as $value)
|
||||
{
|
||||
if (preg_match('/^'. $d['regex'] .'$/mi', $value))
|
||||
break 2;
|
||||
}
|
||||
|
||||
throw new SieveException($token,
|
||||
$d['o_type'] .' '. $d['o_name'] .' requires use of '. $d['type'] .' '. $d['name']);
|
||||
}
|
||||
}
|
||||
}
|
||||
88
data/web/inc/lib/sieve/SieveToken.php
Executable file
88
data/web/inc/lib/sieve/SieveToken.php
Executable file
|
|
@ -0,0 +1,88 @@
|
|||
<?php namespace Sieve;
|
||||
|
||||
include_once('SieveDumpable.php');
|
||||
|
||||
class SieveToken implements SieveDumpable
|
||||
{
|
||||
const Unknown = 0x0000;
|
||||
const ScriptEnd = 0x0001;
|
||||
const LeftBracket = 0x0002;
|
||||
const RightBracket = 0x0004;
|
||||
const BlockStart = 0x0008;
|
||||
const BlockEnd = 0x0010;
|
||||
const LeftParenthesis = 0x0020;
|
||||
const RightParenthesis = 0x0040;
|
||||
const Comma = 0x0080;
|
||||
const Semicolon = 0x0100;
|
||||
const Whitespace = 0x0200;
|
||||
const Tag = 0x0400;
|
||||
const QuotedString = 0x0800;
|
||||
const Number = 0x1000;
|
||||
const Comment = 0x2000;
|
||||
const MultilineString = 0x4000;
|
||||
const Identifier = 0x8000;
|
||||
|
||||
const String = 0x4800; // Quoted | Multiline
|
||||
const StringList = 0x4802; // Quoted | Multiline | LeftBracket
|
||||
const StringListSep = 0x0084; // Comma | RightBracket
|
||||
const Unparsed = 0x2200; // Comment | Whitespace
|
||||
const TestList = 0x8020; // Identifier | LeftParenthesis
|
||||
|
||||
public $type;
|
||||
public $text;
|
||||
public $line;
|
||||
|
||||
public function __construct($type, $text, $line)
|
||||
{
|
||||
$this->text = $text;
|
||||
$this->type = $type;
|
||||
$this->line = intval($line);
|
||||
}
|
||||
|
||||
public function dump()
|
||||
{
|
||||
return '<'. SieveToken::escape($this->text) .'> type:'. SieveToken::typeString($this->type) .' line:'. $this->line;
|
||||
}
|
||||
|
||||
public function text()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
public function is($type)
|
||||
{
|
||||
return (bool)($this->type & $type);
|
||||
}
|
||||
|
||||
public static function typeString($type)
|
||||
{
|
||||
switch ($type)
|
||||
{
|
||||
case SieveToken::Identifier: return 'identifier';
|
||||
case SieveToken::Whitespace: return 'whitespace';
|
||||
case SieveToken::QuotedString: return 'quoted string';
|
||||
case SieveToken::Tag: return 'tag';
|
||||
case SieveToken::Semicolon: return 'semicolon';
|
||||
case SieveToken::LeftBracket: return 'left bracket';
|
||||
case SieveToken::RightBracket: return 'right bracket';
|
||||
case SieveToken::BlockStart: return 'block start';
|
||||
case SieveToken::BlockEnd: return 'block end';
|
||||
case SieveToken::LeftParenthesis: return 'left parenthesis';
|
||||
case SieveToken::RightParenthesis: return 'right parenthesis';
|
||||
case SieveToken::Comma: return 'comma';
|
||||
case SieveToken::Number: return 'number';
|
||||
case SieveToken::Comment: return 'comment';
|
||||
case SieveToken::MultilineString: return 'multiline string';
|
||||
case SieveToken::ScriptEnd: return 'script end';
|
||||
case SieveToken::String: return 'string';
|
||||
case SieveToken::StringList: return 'string list';
|
||||
default: return 'unknown token';
|
||||
}
|
||||
}
|
||||
|
||||
protected static $tr_ = array("\r" => '\r', "\n" => '\n', "\t" => '\t');
|
||||
public static function escape($val)
|
||||
{
|
||||
return strtr($val, self::$tr_);
|
||||
}
|
||||
}
|
||||
117
data/web/inc/lib/sieve/SieveTree.php
Executable file
117
data/web/inc/lib/sieve/SieveTree.php
Executable file
|
|
@ -0,0 +1,117 @@
|
|||
<?php namespace Sieve;
|
||||
|
||||
class SieveTree
|
||||
{
|
||||
protected $childs_;
|
||||
protected $parents_;
|
||||
protected $nodes_;
|
||||
protected $max_id_;
|
||||
protected $dump_;
|
||||
|
||||
public function __construct($name = 'tree')
|
||||
{
|
||||
$this->childs_ = array();
|
||||
$this->parents_ = array();
|
||||
$this->nodes_ = array();
|
||||
$this->max_id_ = 0;
|
||||
|
||||
$this->parents_[0] = null;
|
||||
$this->nodes_[0] = $name;
|
||||
}
|
||||
|
||||
public function addChild(SieveDumpable $child)
|
||||
{
|
||||
return $this->addChildTo($this->max_id_, $child);
|
||||
}
|
||||
|
||||
public function addChildTo($parent_id, SieveDumpable $child)
|
||||
{
|
||||
if (!is_int($parent_id)
|
||||
|| !isset($this->nodes_[$parent_id]))
|
||||
return null;
|
||||
|
||||
if (!isset($this->childs_[$parent_id]))
|
||||
$this->childs_[$parent_id] = array();
|
||||
|
||||
$child_id = ++$this->max_id_;
|
||||
$this->nodes_[$child_id] = $child;
|
||||
$this->parents_[$child_id] = $parent_id;
|
||||
array_push($this->childs_[$parent_id], $child_id);
|
||||
|
||||
return $child_id;
|
||||
}
|
||||
|
||||
public function getRoot()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getChilds($node_id)
|
||||
{
|
||||
if (!is_int($node_id)
|
||||
|| !isset($this->nodes_[$node_id]))
|
||||
return null;
|
||||
|
||||
if (!isset($this->childs_[$node_id]))
|
||||
return array();
|
||||
|
||||
return $this->childs_[$node_id];
|
||||
}
|
||||
|
||||
public function getNode($node_id)
|
||||
{
|
||||
if ($node_id == 0 || !is_int($node_id)
|
||||
|| !isset($this->nodes_[$node_id]))
|
||||
return null;
|
||||
|
||||
return $this->nodes_[$node_id];
|
||||
}
|
||||
|
||||
public function dump()
|
||||
{
|
||||
$this->dump_ = $this->nodes_[$this->getRoot()] ."\n";
|
||||
$this->dumpChilds_($this->getRoot(), ' ');
|
||||
return $this->dump_;
|
||||
}
|
||||
|
||||
protected function dumpChilds_($parent_id, $prefix)
|
||||
{
|
||||
if (!isset($this->childs_[$parent_id]))
|
||||
return;
|
||||
|
||||
$childs = $this->childs_[$parent_id];
|
||||
$last_child = count($childs);
|
||||
|
||||
for ($i=1; $i <= $last_child; ++$i)
|
||||
{
|
||||
$child_node = $this->nodes_[$childs[$i-1]];
|
||||
$infix = ($i == $last_child ? '`--- ' : '|--- ');
|
||||
$this->dump_ .= $prefix . $infix . $child_node->dump() . " (id:" . $childs[$i-1] . ")\n";
|
||||
|
||||
$next_prefix = $prefix . ($i == $last_child ? ' ' : '| ');
|
||||
$this->dumpChilds_($childs[$i-1], $next_prefix);
|
||||
}
|
||||
}
|
||||
|
||||
public function getText()
|
||||
{
|
||||
$this->dump_ = '';
|
||||
$this->childText_($this->getRoot());
|
||||
return $this->dump_;
|
||||
}
|
||||
|
||||
protected function childText_($parent_id)
|
||||
{
|
||||
if (!isset($this->childs_[$parent_id]))
|
||||
return;
|
||||
|
||||
$childs = $this->childs_[$parent_id];
|
||||
|
||||
for ($i = 0; $i < count($childs); ++$i)
|
||||
{
|
||||
$child_node = $this->nodes_[$childs[$i]];
|
||||
$this->dump_ .= $child_node->text();
|
||||
$this->childText_($childs[$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
data/web/inc/lib/sieve/extensions/body.xml
Executable file
14
data/web/inc/lib/sieve/extensions/body.xml
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="body">
|
||||
|
||||
<test name="body">
|
||||
<parameter type="matchtype" occurrence="optional" />
|
||||
<parameter type="comparator" occurrence="optional" />
|
||||
<parameter type="tag" name="body transform" regex="(raw|content|text)" occurrence="optional">
|
||||
<parameter type="stringlist" name="content types" follows="content" />
|
||||
</parameter>
|
||||
<parameter type="stringlist" name="key list" />
|
||||
</test>
|
||||
|
||||
</extension>
|
||||
7
data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml
Executable file
7
data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="comparator-i;ascii-numeric">
|
||||
|
||||
<comparator name="i;ascii-numeric" />
|
||||
|
||||
</extension>
|
||||
9
data/web/inc/lib/sieve/extensions/copy.xml
Executable file
9
data/web/inc/lib/sieve/extensions/copy.xml
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="copy">
|
||||
|
||||
<tagged-argument extends="(fileinto|redirect)">
|
||||
<parameter type="tag" name="copy" regex="copy" occurrence="optional" />
|
||||
</tagged-argument>
|
||||
|
||||
</extension>
|
||||
28
data/web/inc/lib/sieve/extensions/date.xml
Executable file
28
data/web/inc/lib/sieve/extensions/date.xml
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="date">
|
||||
|
||||
<test name="date">
|
||||
<parameter type="matchtype" occurrence="optional" />
|
||||
<parameter type="comparator" occurrence="optional" />
|
||||
<parameter type="tag" name="zone" regex="(zone|originalzone)" occurrence="optional">
|
||||
<parameter type="string" name="time-zone" follows="zone" />
|
||||
</parameter>
|
||||
<parameter type="string" name="header-name" />
|
||||
<parameter type="string" case="ignore" name="date-part"
|
||||
regex="(year|month|day|date|julian|hour|minute|second|time|iso8601|std11|zone|weekday)" />
|
||||
<parameter type="stringlist" name="key-list" />
|
||||
</test>
|
||||
|
||||
<test name="currentdate">
|
||||
<parameter type="matchtype" occurrence="optional" />
|
||||
<parameter type="comparator" occurrence="optional" />
|
||||
<parameter type="tag" name="zone" regex="zone" occurrence="optional">
|
||||
<parameter type="string" name="time-zone" />
|
||||
</parameter>
|
||||
<parameter type="string" case="ignore" name="date-part"
|
||||
regex="(year|month|day|date|julian|hour|minute|second|time|iso8601|std11|zone|weekday)" />
|
||||
<parameter type="stringlist" name="key-list" />
|
||||
</test>
|
||||
|
||||
</extension>
|
||||
9
data/web/inc/lib/sieve/extensions/duplicate.xml
Executable file
9
data/web/inc/lib/sieve/extensions/duplicate.xml
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="duplicate">
|
||||
|
||||
<test name="duplicate">
|
||||
|
||||
</test>
|
||||
|
||||
</extension>
|
||||
22
data/web/inc/lib/sieve/extensions/editheader.xml
Executable file
22
data/web/inc/lib/sieve/extensions/editheader.xml
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="editheader">
|
||||
|
||||
<command name="addheader">
|
||||
<parameter type="tag" name="last" regex="last" occurrence="optional" />
|
||||
<parameter type="string" name="field name" />
|
||||
<parameter type="string" name="value" />
|
||||
</command>
|
||||
|
||||
<command name="deleteheader">
|
||||
<parameter type="tag" name="index" regex="index" occurrence="optional">
|
||||
<parameter type="number" name="field number" />
|
||||
<parameter type="tag" name="last" regex="last" occurrence="optional" />
|
||||
</parameter>
|
||||
<parameter type="matchtype" occurrence="optional" />
|
||||
<parameter type="comparator" occurrence="optional" />
|
||||
<parameter type="string" name="field name" />
|
||||
<parameter type="stringlist" name="value patterns" occurrence="optional" />
|
||||
</command>
|
||||
|
||||
</extension>
|
||||
33
data/web/inc/lib/sieve/extensions/enotify.xml
Executable file
33
data/web/inc/lib/sieve/extensions/enotify.xml
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="enotify">
|
||||
<command name="notify">
|
||||
<parameter type="tag" name="from" occurrence="optional">
|
||||
<parameter type="string" name="from-address" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="importance" regex="(1|2|3)" occurrence="optional" />
|
||||
|
||||
<parameter type="tag" name="options" occurrence="optional">
|
||||
<parameter type="stringlist" name="option-strings" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="message" occurrence="optional">
|
||||
<parameter type="string" name="message-text" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="string" name="method" />
|
||||
</command>
|
||||
|
||||
<test name="valid_notify_method">
|
||||
<parameter type="stringlist" name="notification-uris" />
|
||||
</test>
|
||||
|
||||
<test name="notify_method_capability">
|
||||
<parameter type="string" name="notification-uri" />
|
||||
<parameter type="string" name="notification-capability" />
|
||||
<parameter type="stringlist" name="key-list" />
|
||||
</test>
|
||||
|
||||
<modifier name="encodeurl" />
|
||||
</extension>
|
||||
13
data/web/inc/lib/sieve/extensions/envelope.xml
Executable file
13
data/web/inc/lib/sieve/extensions/envelope.xml
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="envelope">
|
||||
|
||||
<test name="envelope">
|
||||
<parameter type="matchtype" occurrence="optional" />
|
||||
<parameter type="comparator" occurrence="optional" />
|
||||
<parameter type="addresspart" occurrence="optional" />
|
||||
<parameter type="stringlist" name="envelope-part" />
|
||||
<parameter type="stringlist" name="key" />
|
||||
</test>
|
||||
|
||||
</extension>
|
||||
13
data/web/inc/lib/sieve/extensions/environment.xml
Executable file
13
data/web/inc/lib/sieve/extensions/environment.xml
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="environment">
|
||||
|
||||
<test name="environment">
|
||||
<parameter type="matchtype" occurrence="optional" />
|
||||
<parameter type="comparator" occurrence="optional" />
|
||||
<parameter type="string" name="name"
|
||||
regex="(domain|host|location|name|phase|remote-host|remote-ip|version|vnd\..+)" />
|
||||
<parameter type="stringlist" name="key-list" />
|
||||
</test>
|
||||
|
||||
</extension>
|
||||
11
data/web/inc/lib/sieve/extensions/ereject.xml
Executable file
11
data/web/inc/lib/sieve/extensions/ereject.xml
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="ereject">
|
||||
|
||||
<command name="ereject">
|
||||
|
||||
<parameter type="string" name="reason" />
|
||||
|
||||
</command>
|
||||
|
||||
</extension>
|
||||
10
data/web/inc/lib/sieve/extensions/fileinto.xml
Executable file
10
data/web/inc/lib/sieve/extensions/fileinto.xml
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="fileinto">
|
||||
|
||||
<command name="fileinto">
|
||||
<parameter type="tag" name="create" regex="create" occurrence="optional" />
|
||||
<parameter type="string" name="folder" />
|
||||
</command>
|
||||
|
||||
</extension>
|
||||
29
data/web/inc/lib/sieve/extensions/imap4flags.xml
Executable file
29
data/web/inc/lib/sieve/extensions/imap4flags.xml
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="imap4flags">
|
||||
|
||||
<command name="setflag">
|
||||
<parameter type="stringlist" name="flag list" />
|
||||
</command>
|
||||
|
||||
<command name="addflag">
|
||||
<parameter type="stringlist" name="flag list" />
|
||||
</command>
|
||||
|
||||
<command name="removeflag">
|
||||
<parameter type="stringlist" name="flag list" />
|
||||
</command>
|
||||
|
||||
<test name="hasflag">
|
||||
<parameter type="matchtype" occurrence="optional" />
|
||||
<parameter type="comparator" occurrence="optional" />
|
||||
<parameter type="stringlist" name="flag list" />
|
||||
</test>
|
||||
|
||||
<tagged-argument extends="(fileinto|keep)">
|
||||
<parameter type="tag" name="flags" regex="flags" occurrence="optional">
|
||||
<parameter type="stringlist" name="flag list" />
|
||||
</parameter>
|
||||
</tagged-argument>
|
||||
|
||||
</extension>
|
||||
21
data/web/inc/lib/sieve/extensions/imapflags.xml
Executable file
21
data/web/inc/lib/sieve/extensions/imapflags.xml
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="imapflags">
|
||||
|
||||
<command name="mark" />
|
||||
|
||||
<command name="unmark" />
|
||||
|
||||
<command name="setflag">
|
||||
<parameter type="stringlist" name="flag list" />
|
||||
</command>
|
||||
|
||||
<command name="addflag">
|
||||
<parameter type="stringlist" name="flag list" />
|
||||
</command>
|
||||
|
||||
<command name="removeflag">
|
||||
<parameter type="stringlist" name="flag list" />
|
||||
</command>
|
||||
|
||||
</extension>
|
||||
17
data/web/inc/lib/sieve/extensions/index.xml
Executable file
17
data/web/inc/lib/sieve/extensions/index.xml
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="index">
|
||||
|
||||
<tagged-argument extends="(header|address|date)">
|
||||
<parameter type="tag" name="index" regex="index" occurrence="optional">
|
||||
<parameter type="number" name="field number" />
|
||||
</parameter>
|
||||
</tagged-argument>
|
||||
|
||||
<tagged-argument extends="(header|address|date)">
|
||||
<parameter type="tag" name="last" regex="last" occurrence="optional">
|
||||
<requires type="tag" name="index" regex="index" />
|
||||
</parameter>
|
||||
</tagged-argument>
|
||||
|
||||
</extension>
|
||||
8
data/web/inc/lib/sieve/extensions/mailbox.xml
Executable file
8
data/web/inc/lib/sieve/extensions/mailbox.xml
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="mailbox">
|
||||
|
||||
<test name="mailboxexists">
|
||||
<parameter type="string" name="folder" />
|
||||
</test>
|
||||
</extension>
|
||||
58
data/web/inc/lib/sieve/extensions/mime.xml
Executable file
58
data/web/inc/lib/sieve/extensions/mime.xml
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="mime">
|
||||
<command name="foreverypart">
|
||||
<parameter type="string" name="name" occurrence="optional" />
|
||||
<block />
|
||||
</command>
|
||||
|
||||
<command name="break">
|
||||
<parameter type="string" name="name" occurrence="optional" />
|
||||
</command>
|
||||
|
||||
<tagged-argument extends="(header|address|exists)">
|
||||
<parameter type="tag" name="mime" regex="mime" occurrence="optional" />
|
||||
</tagged-argument>
|
||||
<tagged-argument extends="(header|address|exists)">
|
||||
<parameter type="tag" name="anychild" regex="anychild" occurrence="optional" />
|
||||
</tagged-argument>
|
||||
<tagged-argument extends="(header)">
|
||||
<parameter type="tag" name="type" occurrence="optional" />
|
||||
</tagged-argument>
|
||||
<tagged-argument extends="(header)">
|
||||
<parameter type="tag" name="subtype" occurrence="optional" />
|
||||
</tagged-argument>
|
||||
<tagged-argument extends="(header)">
|
||||
<parameter type="tag" name="contenttype" occurrence="optional" />
|
||||
</tagged-argument>
|
||||
<tagged-argument extends="(header)">
|
||||
<parameter type="tag" name="param" regex="param" occurrence="optional">
|
||||
<parameter type="stringlist" name="param-list" />
|
||||
</parameter>
|
||||
</tagged-argument>
|
||||
<tagged-argument extends="(header|address|exists)">
|
||||
<parameter type="stringlist" name="header-names" />
|
||||
</tagged-argument>
|
||||
<tagged-argument extends="(header)">
|
||||
<parameter type="stringlist" name="key-list" />
|
||||
</tagged-argument>
|
||||
|
||||
<action name="replace">
|
||||
<parameter type="tag" name="mime" regex="mime" occurrence="optional" />
|
||||
<parameter type="string" name="subject" occurrence="optional" />
|
||||
<parameter type="string" name="from" occurrence="optional" />
|
||||
<parameter type="string" name="replacement" />
|
||||
</action>
|
||||
|
||||
<action name="enclose">
|
||||
<parameter type="string" name="subject" occurrence="optional" />
|
||||
<parameter type="stringlist" name="headers" occurrence="optional" />
|
||||
<parameter type="string" name="text" />
|
||||
</action>
|
||||
|
||||
<action name="extracttext">
|
||||
<parameter type="tag" name="first" regex="first" occurrence="optional" />
|
||||
<parameter type="number" name="number" occurrence="optional" />
|
||||
<parameter type="string" name="varname" />
|
||||
</action>
|
||||
</extension>
|
||||
29
data/web/inc/lib/sieve/extensions/notify.xml
Executable file
29
data/web/inc/lib/sieve/extensions/notify.xml
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="notify">
|
||||
|
||||
<command name="notify">
|
||||
<parameter type="tag" name="method" regex="method" occurrence="optional">
|
||||
<parameter type="string" name="method-name" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="id" regex="id" occurrence="optional">
|
||||
<parameter type="string" name="message-id" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="priority" regex="(low|normal|high)" occurrence="optional" />
|
||||
|
||||
<parameter type="tag" name="message" regex="message" occurrence="optional">
|
||||
<parameter type="string" name="message-text" />
|
||||
</parameter>
|
||||
</command>
|
||||
|
||||
<command name="denotify">
|
||||
<parameter type="matchtype" occurrence="optional">
|
||||
<parameter type="string" name="message-id" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="priority" regex="(low|normal|high)" occurrence="optional" />
|
||||
</command>
|
||||
|
||||
</extension>
|
||||
11
data/web/inc/lib/sieve/extensions/regex.xml
Executable file
11
data/web/inc/lib/sieve/extensions/regex.xml
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="regex">
|
||||
|
||||
<matchtype name="regex" />
|
||||
|
||||
<tagged-argument extends="set">
|
||||
<parameter type="tag" name="modifier" regex="quoteregex" occurrence="optional" />
|
||||
</tagged-argument>
|
||||
|
||||
</extension>
|
||||
11
data/web/inc/lib/sieve/extensions/reject.xml
Executable file
11
data/web/inc/lib/sieve/extensions/reject.xml
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="reject">
|
||||
|
||||
<command name="reject">
|
||||
|
||||
<parameter type="string" name="reason" />
|
||||
|
||||
</command>
|
||||
|
||||
</extension>
|
||||
14
data/web/inc/lib/sieve/extensions/relational.xml
Executable file
14
data/web/inc/lib/sieve/extensions/relational.xml
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="relational">
|
||||
|
||||
<matchtype name="count">
|
||||
<requires type="comparator" name="i;ascii-numeric" regex="i;ascii-numeric" />
|
||||
<parameter type="string" name="relation string" regex="(lt|le|eq|ge|gt|ne)" />
|
||||
</matchtype>
|
||||
|
||||
<matchtype name="value">
|
||||
<parameter type="string" name="relation string" regex="(lt|le|eq|ge|gt|ne)" />
|
||||
</matchtype>
|
||||
|
||||
</extension>
|
||||
11
data/web/inc/lib/sieve/extensions/spamtest.xml
Executable file
11
data/web/inc/lib/sieve/extensions/spamtest.xml
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="spamtest">
|
||||
|
||||
<test name="spamtest">
|
||||
<parameter type="comparator" occurrence="optional" />
|
||||
<parameter type="matchtype" occurrence="optional" />
|
||||
<parameter type="string" name="value" />
|
||||
</test>
|
||||
|
||||
</extension>
|
||||
12
data/web/inc/lib/sieve/extensions/spamtestplus.xml
Executable file
12
data/web/inc/lib/sieve/extensions/spamtestplus.xml
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="spamtestplus">
|
||||
|
||||
<test name="spamtest" overrides="true">
|
||||
<parameter type="comparator" occurrence="optional" />
|
||||
<parameter type="matchtype" occurrence="optional" />
|
||||
<parameter type="tag" name="percent" regex="percent" occurrence="optional" />
|
||||
<parameter type="string" name="value" />
|
||||
</test>
|
||||
|
||||
</extension>
|
||||
8
data/web/inc/lib/sieve/extensions/subaddress.xml
Executable file
8
data/web/inc/lib/sieve/extensions/subaddress.xml
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="subaddress">
|
||||
|
||||
<addresspart name="user" />
|
||||
<addresspart name="detail" />
|
||||
|
||||
</extension>
|
||||
32
data/web/inc/lib/sieve/extensions/vacation-seconds.xml
Executable file
32
data/web/inc/lib/sieve/extensions/vacation-seconds.xml
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="vacation-seconds">
|
||||
|
||||
<command name="vacation">
|
||||
|
||||
<parameter type="tag" name="seconds" occurrence="optional" regex="seconds">
|
||||
<parameter type="number" name="period" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="addresses" occurrence="optional" regex="addresses">
|
||||
<parameter type="stringlist" name="address strings" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="subject" occurrence="optional" regex="subject">
|
||||
<parameter type="string" name="subject string" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="from" occurrence="optional" regex="from">
|
||||
<parameter type="string" name="from string" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="handle" occurrence="optional" regex="handle">
|
||||
<parameter type="string" name="handle string" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="mime" occurrence="optional" regex="mime" />
|
||||
|
||||
<parameter type="string" name="reason" />
|
||||
</command>
|
||||
|
||||
</extension>
|
||||
31
data/web/inc/lib/sieve/extensions/vacation.xml
Executable file
31
data/web/inc/lib/sieve/extensions/vacation.xml
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version='1.0' standalone='yes'?>
|
||||
|
||||
<extension name="vacation">
|
||||
|
||||
<command name="vacation">
|
||||
<parameter type="tag" name="days" occurrence="optional" regex="days">
|
||||
<parameter type="number" name="period" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="addresses" occurrence="optional" regex="addresses">
|
||||
<parameter type="stringlist" name="address strings" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="subject" occurrence="optional" regex="subject">
|
||||
<parameter type="string" name="subject string" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="from" occurrence="optional" regex="from">
|
||||
<parameter type="string" name="from string" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="handle" occurrence="optional" regex="handle">
|
||||
<parameter type="string" name="handle string" />
|
||||
</parameter>
|
||||
|
||||
<parameter type="tag" name="mime" occurrence="optional" regex="mime" />
|
||||
|
||||
<parameter type="string" name="reason" />
|
||||
</command>
|
||||
|
||||
</extension>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue