This commit is contained in:
traveler 2025-04-17 17:29:05 -05:00
commit 5aa7d034f7
3292 changed files with 465160 additions and 0 deletions

View 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)));
}
}
?>

View 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']);
?>

View 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();
}
?>

View 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
View 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']);
}
?>

View file

@ -0,0 +1,3 @@
<?php
session_start();
$_SESSION['show_rspamd_global_filters'] = true;

View 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']));
}
?>

View 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'];
}
}
?>

View 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>&nbsp;&nbsp;&nbsp;&nbsp;↪ ' . 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
View 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;

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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);
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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
View 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

File diff suppressed because it is too large Load diff

View 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);
}
}

View 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);
}
}

View 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();
}
}

View 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;
}
}

View 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;
}
}

View 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));
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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), '+/', '-_'), '=');
}
}

View 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;
}
}

View 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 credentials 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;
}
}

View 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);
}
}

View 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-----

Binary file not shown.

View 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-----

View 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-----

View 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-----

View 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-----

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBmjCCAT8CFBZiBJbp2fT/LaRJ8Xwl9qhX62boMAoGCCqGSM49BAMCME4xCzAJ
BgNVBAYTAkRFMRYwFAYDVQQKDA1OaXRyb2tleSBHbWJIMRAwDgYDVQQLDAdSb290
IENBMRUwEwYDVQQDDAxuaXRyb2tleS5jb20wIBcNMTkxMjA0MDczNTM1WhgPMjA2
OTExMjEwNzM1MzVaME4xCzAJBgNVBAYTAkRFMRYwFAYDVQQKDA1OaXRyb2tleSBH
bWJIMRAwDgYDVQQLDAdSb290IENBMRUwEwYDVQQDDAxuaXRyb2tleS5jb20wWTAT
BgcqhkjOPQIBBggqhkjOPQMBBwNCAAQy6KIN2gXqaSMWdWir/Hnx58NBzjthYdNv
k95hdt7jCpyW2cHqLdQ5Sqcvo0CuordgDOach0ZGB60w9GZY8SHJMAoGCCqGSM49
BAMCA0kAMEYCIQDLmdy2G2mM4rZKjl6CVfjV7khilIS5D3xRQzubeqzQNAIhAKIG
X29SfiB6K9k6Hb3q+q7bRn1o1dhV1cj592YYnu1/
-----END CERTIFICATE-----

View 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-----

View 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-----

View 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
View 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 &timestamp=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');
}
}
?>

View 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
View 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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
<?php namespace Sieve;
interface SieveDumpable
{
function dump();
function text();
}

View 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;
}
}

View 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_);
}
}

View 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);
}
}

View 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]+'
);
}

View file

@ -0,0 +1,6 @@
<?php namespace Sieve;
class SieveScript
{
// TODO: implement
}

View 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']);
}
}
}

View 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_);
}
}

View 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]);
}
}
}

View 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>

View file

@ -0,0 +1,7 @@
<?xml version='1.0' standalone='yes'?>
<extension name="comparator-i;ascii-numeric">
<comparator name="i;ascii-numeric" />
</extension>

View 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>

View 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>

View file

@ -0,0 +1,9 @@
<?xml version='1.0' standalone='yes'?>
<extension name="duplicate">
<test name="duplicate">
</test>
</extension>

View 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>

View 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>

View 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>

View 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>

View file

@ -0,0 +1,11 @@
<?xml version='1.0' standalone='yes'?>
<extension name="ereject">
<command name="ereject">
<parameter type="string" name="reason" />
</command>
</extension>

View 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>

View 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>

View 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>

View 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>

View file

@ -0,0 +1,8 @@
<?xml version='1.0' standalone='yes'?>
<extension name="mailbox">
<test name="mailboxexists">
<parameter type="string" name="folder" />
</test>
</extension>

View 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>

View 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>

View 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>

View file

@ -0,0 +1,11 @@
<?xml version='1.0' standalone='yes'?>
<extension name="reject">
<command name="reject">
<parameter type="string" name="reason" />
</command>
</extension>

View 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>

View 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>

View 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>

View file

@ -0,0 +1,8 @@
<?xml version='1.0' standalone='yes'?>
<extension name="subaddress">
<addresspart name="user" />
<addresspart name="detail" />
</extension>

View 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>

View 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