From 32176c1a3222beccd0067708d4eb78b5a3ccaa77 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 27 Mar 2017 22:33:32 +0200 Subject: [PATCH 001/108] Add hint for local MTA and port blocking issues --- docs/first_steps.md | 10 ++++++++++ docs/install.md | 2 ++ 2 files changed, 12 insertions(+) diff --git a/docs/first_steps.md b/docs/first_steps.md index 7f835bc4..21c418db 100644 --- a/docs/first_steps.md +++ b/docs/first_steps.md @@ -136,6 +136,16 @@ server { } ``` +## Install a local MTA + +The easiest option would be to disable the listener on port 25/tcp. + +**Postfix** users disable the listener by commenting the following line (starting with `smtp` or `25`) in `/etc/postfix/master.cf`: +``` +#smtp inet n - - - - smtpd +``` +Restart Postfix after applying your changes. + ## Sender and receiver model When a mailbox is created, a user is allowed to send mail from and receive mail for his own mailbox address. diff --git a/docs/install.md b/docs/install.md index 7c383a63..6833f323 100644 --- a/docs/install.md +++ b/docs/install.md @@ -35,6 +35,8 @@ nano mailcow.conf ``` If you plan to use a reverse proxy, you can, for example, bind HTTPS to 127.0.0.1 on port 8443 and HTTP to 127.0.0.1 on port 8080. +You may need to stop an existing pre-installed MTA which blocks port 25/tcp. See [this chapter](https://andryyy.github.io/mailcow-dockerized/first_steps/#install-a-local-mta) to learn how to reconfigure Postfix to run besides mailcow after a successful installation. + 5\. Pull the images and run the composer file. The paramter `-d` will start mailcow: dockerized detached: ``` docker-compose pull From d3c0d0c16e64bbb8aa969a6cac1d5eeb8af3bc71 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 28 Mar 2017 11:47:03 +0200 Subject: [PATCH 002/108] Use footable for domain admins table --- data/web/admin.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/data/web/admin.php b/data/web/admin.php index 5377616f..e0e04aee 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -81,14 +81,14 @@ $tfa_data = get_tfa();
- +
- - - - - + + + + + @@ -299,8 +299,14 @@ $tfa_data = get_tfa(); - - + + Date: Tue, 28 Mar 2017 11:48:39 +0200 Subject: [PATCH 003/108] Show aliases left, check if quota 0 when creating domains, return modified instead of created for domain admin --- data/web/inc/functions.inc.php | 70 +++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index c37e39ab..cbc71102 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1757,6 +1757,7 @@ function get_domain_admin_details($domain_admin) { try { $stmt = $pdo->prepare("SELECT `tfa`.`active` AS `tfa_active_int`, + CASE `tfa`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `tfa_active`, `domain_admins`.`username`, `domain_admins`.`created`, `domain_admins`.`active` AS `active_int`, @@ -1768,11 +1769,15 @@ function get_domain_admin_details($domain_admin) { ':domain_admin' => $domain_admin )); $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)) { + return false; + } $domainadmindata['username'] = $row['username']; + $domainadmindata['tfa_active'] = $row['tfa_active']; $domainadmindata['active'] = $row['active']; - $domainadmindata['active_int'] = $row['active_int']; $domainadmindata['tfa_active_int'] = $row['tfa_active_int']; - $domainadmindata['created'] = $row['created']; + $domainadmindata['active_int'] = $row['active_int']; + $domainadmindata['modified'] = $row['created']; // GET SELECTED $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain` IN ( @@ -1793,6 +1798,9 @@ function get_domain_admin_details($domain_admin) { while($row = array_shift($rows)) { $domainadmindata['unselected_domains'][] = $row['domain']; } + if (!isset($domainadmindata['unselected_domains'])) { + $domainadmindata['unselected_domains'] = ""; + } } catch(PDOException $e) { $_SESSION['return'] = array( @@ -2134,6 +2142,14 @@ function edit_domain_admin($postarray) { } } + if (empty($postarray['domain'])) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['domain_invalid']) + ); + return false; + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) { $_SESSION['return'] = array( 'type' => 'danger', @@ -2164,7 +2180,7 @@ function edit_domain_admin($postarray) { return false; } - if(isset($postarray['domain'])) { + if (isset($postarray['domain'])) { foreach ($postarray['domain'] as $domain) { try { $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) @@ -2519,6 +2535,14 @@ function mailbox_add_domain($postarray) { return false; } + if ($maxquota == "0" || empty($maxquota)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['maxquota_empty']) + ); + return false; + } + isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['relay_all_recipients']) ? $relay_all_recipients = '1' : $relay_all_recipients = '0'; isset($postarray['backupmx']) ? $backupmx = '1' : $backupmx = '0'; @@ -2623,6 +2647,18 @@ function mailbox_add_alias($postarray) { return false; } + $stmt = $pdo->prepare("SELECT `address` FROM `alias` + WHERE `address`= :address"); + $stmt->execute(array(':address' => $address)); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['is_alias_or_mailbox'], htmlspecialchars($address)) + ); + return false; + } + foreach ($addresses as $address) { if (empty($address)) { continue; @@ -2632,6 +2668,15 @@ function mailbox_add_alias($postarray) { $local_part = strstr($address, '@', true); $address = $local_part.'@'.$domain; + $domaindata = mailbox_get_domain_details($domain); + if ($domaindata['aliases_left'] == 0) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['max_alias_exceeded']) + ); + return false; + } + try { $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)"); @@ -3432,7 +3477,7 @@ function mailbox_edit_domain($postarray) { // aliases float // mailboxes float // maxquota float - // quota float (Byte) + // quota float (Byte) // active int global $lang; @@ -3519,6 +3564,14 @@ function mailbox_edit_domain($postarray) { return false; } + if ($maxquota == "0" || empty($maxquota)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['maxquota_empty']) + ); + return false; + } + if ($MailboxData['maxquota'] > $maxquota) { $_SESSION['return'] = array( 'type' => 'danger', @@ -4271,6 +4324,10 @@ function mailbox_get_domain_details($domain) { ':domain' => $domain, )); $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)) { + return false; + } + $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` = :domain"); $stmt->execute(array(':domain' => $row['domain'])); $MailboxDataDomain = $stmt->fetch(PDO::FETCH_ASSOC); @@ -4303,8 +4360,9 @@ function mailbox_get_domain_details($domain) { $stmt->execute(array( ':domain' => $domain, )); - $AliasData = $stmt->fetch(PDO::FETCH_ASSOC); - (isset($AliasData['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasData['alias_count'] : $domaindata['aliases_in_domain'] = "0"; + $AliasDataDomain = $stmt->fetch(PDO::FETCH_ASSOC); + (isset($AliasDataDomain['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasDataDomain['alias_count'] : $domaindata['aliases_in_domain'] = "0"; + $domaindata['aliases_left'] = $row['aliases'] - $AliasDataDomain['alias_count']; } catch (PDOException $e) { $_SESSION['return'] = array( From 297674d2569a713ef2d6426e66830256f9397835 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 28 Mar 2017 11:51:17 +0200 Subject: [PATCH 004/108] Various CSS fixes, remove sorttable, better API format --- data/web/css/mailbox.css | 28 +-- data/web/css/tables.css | 79 --------- data/web/inc/footer.inc.php | 12 +- data/web/inc/header.inc.php | 2 - data/web/js/add.js | 2 +- data/web/js/admin.js | 69 ++++---- data/web/js/mailbox.js | 22 +-- data/web/js/sorttable.js | 236 ------------------------- data/web/json_api.php | 336 ++++++++++++++++++++++-------------- data/web/lang/lang.de.php | 5 + data/web/lang/lang.en.php | 5 + data/web/mailbox.php | 34 +--- data/web/user.php | 4 +- 13 files changed, 293 insertions(+), 541 deletions(-) delete mode 100644 data/web/css/tables.css delete mode 100644 data/web/js/sorttable.js diff --git a/data/web/css/mailbox.css b/data/web/css/mailbox.css index b5c69343..23f5ad84 100644 --- a/data/web/css/mailbox.css +++ b/data/web/css/mailbox.css @@ -1,19 +1,19 @@ -.panel-heading div { - margin-top: -18px; - font-size: 15px; +table.footable>tbody>tr.footable-empty>td { + font-size:15px !important; + font-style:italic; } -.panel-heading div span { - margin-left:5px; +.pagination a { + text-decoration: none !important; } -.panel-body { - display: none; +.panel panel-default { + overflow: visible !important; } -.clickable { - cursor: pointer; +.table-responsive { + overflow: visible !important; } -.progress { - margin-bottom: 0px; -} -.table>thead>tr>th { - vertical-align: top !important; +.footer-add-item { + text-align:center; + font-style: italic; + display:block; + padding: 10px; } \ No newline at end of file diff --git a/data/web/css/tables.css b/data/web/css/tables.css deleted file mode 100644 index 651e1665..00000000 --- a/data/web/css/tables.css +++ /dev/null @@ -1,79 +0,0 @@ -ul[id*="sortable"] { word-wrap: break-word; list-style-type: none; float: left; padding: 0 15px 0 0; width: 48%; cursor:move} -ul[id$="sortable-active"] li {cursor:move; } -ul[id$="sortable-inactive"] li {cursor:move } -.list-heading { cursor:default !important} -.ui-state-disabled { cursor:no-drop; color:#ccc; } -.ui-state-highlight {background: #F5F5F5 !important; height: 41px !important; cursor:move } -table[data-sortable] { - border-collapse: collapse; - border-spacing: 0; -} -table[data-sortable] th { - vertical-align: bottom; - font-weight: bold; -} -table[data-sortable] th, table[data-sortable] td { - text-align: left; - padding: 10px; -} -table[data-sortable] th:not([data-sortable="false"]) { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - -webkit-touch-callout: none; - cursor: pointer; -} -table[data-sortable] th:after { - content: ""; - visibility: hidden; - display: inline-block; - vertical-align: inherit; - height: 0; - width: 0; - border-width: 5px; - border-style: solid; - border-color: transparent; - margin-right: 1px; - margin-left: 10px; - float: right; -} -table[data-sortable] th[data-sortable="false"]:after { - display: none; -} -table[data-sortable] th[data-sorted="true"]:after { - visibility: visible; -} -table[data-sortable] th[data-sorted-direction="descending"]:after { - border-top-color: inherit; - margin-top: 8px; -} -table[data-sortable] th[data-sorted-direction="ascending"]:after { - border-bottom-color: inherit; - margin-top: 3px; -} -table[data-sortable].sortable-theme-bootstrap thead th { - border-bottom: 2px solid #e0e0e0; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"] { - color: #3a87ad; - background: #d9edf7; - border-bottom-color: #bce8f1; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="descending"]:after { - border-top-color: #3a87ad; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="ascending"]:after { - border-bottom-color: #3a87ad; -} -table[data-sortable].sortable-theme-bootstrap.sortable-theme-bootstrap-striped tbody > tr:nth-child(odd) > td { - background-color: #f9f9f9; -} -#data td, #no-data td { - vertical-align: middle; -} -.sort-table:hover { - border-bottom-color: #00B7DC !important; -} \ No newline at end of file diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 5523ec58..580c457f 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -50,11 +50,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "json_api.php", - data: { - 'action':'get_u2f_auth_challenge', - 'object':'', - }, + url: "/api/v1/u2f-authentication/", success: function(data){ data; } @@ -87,11 +83,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "json_api.php", - data: { - 'action':'get_u2f_reg_challenge', - 'object':'', - }, + url: "/api/v1/u2f-registration/", success: function(data){ data; } diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index a206a35d..a7ec1c52 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -16,10 +16,8 @@ - - ' : null;?> diff --git a/data/web/js/add.js b/data/web/js/add.js index 1ebcdbb2..54a70500 100644 --- a/data/web/js/add.js +++ b/data/web/js/add.js @@ -2,7 +2,7 @@ $(document).ready(function() { // add.php // Get max. possible quota for a domain when domain field changes $('#addSelectDomain').on('change', function() { - $.get("json_api.php", { action:"get_domain_details", object:this.value }, function(data){ + $.get("/api/v1/domain/" + this.value, function(data){ var result = jQuery.parseJSON( data ); max_new_mailbox_quota = ( result.max_new_mailbox_quota / 1048576); if (max_new_mailbox_quota != '0') { diff --git a/data/web/js/admin.js b/data/web/js/admin.js index a235a422..feaf3907 100644 --- a/data/web/js/admin.js +++ b/data/web/js/admin.js @@ -1,31 +1,42 @@ $(document).ready(function() { - // Postfix restrictions, drag and drop functions - $( "[id*=srr-sortable]" ).sortable({ - items: "li:not(.list-heading)", - cancel: ".ui-state-disabled", - connectWith: "[id*=srr-sortable]", - dropOnEmpty: true, - placeholder: "ui-state-highlight" - }); - $( "[id*=ssr-sortable]" ).sortable({ - items: "li:not(.list-heading)", - cancel: ".ui-state-disabled", - connectWith: "[id*=ssr-sortable]", - dropOnEmpty: true, - placeholder: "ui-state-highlight" - }); - $('#srr_form').submit(function(){ - var srr_joined_vals = $("[id^=srr-sortable-active] li").map(function() { - return $(this).data("value"); - }).get().join(', '); - var input = $("").attr("type", "hidden").attr("name", "srr_value").val(srr_joined_vals); - $('#srr_form').append($(input)); - }); - $('#ssr_form').submit(function(){ - var ssr_joined_vals = $("[id^=ssr-sortable-active] li").map(function() { - return $(this).data("value"); - }).get().join(', '); - var input = $("").attr("type", "hidden").attr("name", "ssr_value").val(ssr_joined_vals); - $('#ssr_form').append($(input)); - }); + $.ajax({ + dataType: 'json', + url: '/api/v1/domain-admin/all', + jsonp: false, + error: function () { + alert('Cannot draw domain administrator table'); + }, + success: function (data) { + $.each(data, function (i, item) { + item.action = ''; + }); + $('#domainadminstable').footable({ + "columns": [ + {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}}, + {"name":"selected_domains","title":lang.admin_domains,"breakpoints":"xs sm"}, + {"name":"tfa_active","title":"TFA", "filterable": false,"style":{"maxWidth":"80px","width":"80px"}}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + ], + "rows": data, + "empty": lang.empty, + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "filtering": { + "enabled": true, + "position": "left", + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + } + }); + } + }); }); \ No newline at end of file diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js index 1ddf0c35..4e6b303d 100644 --- a/data/web/js/mailbox.js +++ b/data/web/js/mailbox.js @@ -15,7 +15,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=domain_table_data', + url: '/api/v1/domain/all', jsonp: false, error: function () { alert('Cannot draw domain table'); @@ -70,7 +70,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=mailbox_table_data', + url: '/api/v1/mailbox/all', jsonp: false, error: function () { alert('Cannot draw mailbox table'); @@ -102,12 +102,12 @@ $(document).ready(function() { {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}}, {"name":"name","title":lang.fname,"breakpoints":"xs sm"}, {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, - {"name":"quota","title":lang.domain_quota}, - {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm"}, - {"name":"in_use","filterable": false,"type":"html","title":lang.in_use}, - {"name":"messages","filterable": false,"style":{"width":"90px"},"title":lang.msg_num,"breakpoints":"xs sm"}, - {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","width":"290px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota}, + {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm md"}, + {"name":"in_use","filterable": false,"style":{"whiteSpace":"nowrap"},"type":"html","title":lang.in_use}, + {"name":"messages","filterable": false,"style":{"whiteSpace":"nowrap"},"title":lang.msg_num,"breakpoints":"xs sm md"}, + {"name":"active","filterable": false,"style":{"whiteSpace":"nowrap"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"whiteSpace":"nowrap","text-align":"right","width":"290px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} ], "empty": lang.empty, "rows": data, @@ -130,7 +130,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=resource_table_data', + url: '/api/v1/resource/all', jsonp: false, error: function () { alert('Cannot draw resource table'); @@ -172,7 +172,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=domain_alias_table_data', + url: '/api/v1/alias-domain/all', jsonp: false, error: function () { alert('Cannot draw alias domain table'); @@ -212,7 +212,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=alias_table_data', + url: '/api/v1/alias/all', jsonp: false, error: function () { alert('Cannot draw alias table'); diff --git a/data/web/js/sorttable.js b/data/web/js/sorttable.js deleted file mode 100644 index cb3e293c..00000000 --- a/data/web/js/sorttable.js +++ /dev/null @@ -1,236 +0,0 @@ -(function() { - var SELECTOR, addEventListener, clickEvents, numberRegExp, sortable, touchDevice, trimRegExp; - - SELECTOR = 'table[data-sortable]'; - - numberRegExp = /^-?[£$¤]?[\d,.]+%?$/; - - trimRegExp = /^\s+|\s+$/g; - - clickEvents = ['click']; - - touchDevice = 'ontouchstart' in document.documentElement; - - if (touchDevice) { - clickEvents.push('touchstart'); - } - - addEventListener = function(el, event, handler) { - if (el.addEventListener != null) { - return el.addEventListener(event, handler, false); - } else { - return el.attachEvent("on" + event, handler); - } - }; - - sortable = { - init: function(options) { - var table, tables, _i, _len, _results; - if (options == null) { - options = {}; - } - if (options.selector == null) { - options.selector = SELECTOR; - } - tables = document.querySelectorAll(options.selector); - _results = []; - for (_i = 0, _len = tables.length; _i < _len; _i++) { - table = tables[_i]; - _results.push(sortable.initTable(table)); - } - return _results; - }, - initTable: function(table) { - var i, th, ths, _i, _len, _ref; - if (((_ref = table.tHead) != null ? _ref.rows.length : void 0) !== 1) { - return; - } - if (table.getAttribute('data-sortable-initialized') === 'true') { - return; - } - table.setAttribute('data-sortable-initialized', 'true'); - ths = table.querySelectorAll('th'); - for (i = _i = 0, _len = ths.length; _i < _len; i = ++_i) { - th = ths[i]; - if (th.getAttribute('data-sortable') !== 'false') { - sortable.setupClickableTH(table, th, i); - } - } - return table; - }, - setupClickableTH: function(table, th, i) { - var eventName, onClick, type, _i, _len, _results; - type = sortable.getColumnType(table, i); - onClick = function(e) { - var compare, item, newSortedDirection, position, row, rowArray, sorted, sortedDirection, tBody, ths, value, _compare, _i, _j, _k, _l, _len, _len1, _len2, _len3, _len4, _m, _ref, _ref1; - if (e.handled !== true) { - e.handled = true; - } else { - return false; - } - sorted = this.getAttribute('data-sorted') === 'true'; - sortedDirection = this.getAttribute('data-sorted-direction'); - if (sorted) { - newSortedDirection = sortedDirection === 'ascending' ? 'descending' : 'ascending'; - } else { - newSortedDirection = type.defaultSortDirection; - } - ths = this.parentNode.querySelectorAll('th'); - for (_i = 0, _len = ths.length; _i < _len; _i++) { - th = ths[_i]; - th.setAttribute('data-sorted', 'false'); - th.removeAttribute('data-sorted-direction'); - } - this.setAttribute('data-sorted', 'true'); - this.setAttribute('data-sorted-direction', newSortedDirection); - tBody = table.tBodies[0]; - rowArray = []; - if (!sorted) { - if (type.compare != null) { - _compare = type.compare; - } else { - _compare = function(a, b) { - return b - a; - }; - } - compare = function(a, b) { - if (a[0] === b[0]) { - return a[2] - b[2]; - } - if (type.reverse) { - return _compare(b[0], a[0]); - } else { - return _compare(a[0], b[0]); - } - }; - _ref = tBody.rows; - for (position = _j = 0, _len1 = _ref.length; _j < _len1; position = ++_j) { - row = _ref[position]; - value = sortable.getNodeValue(row.cells[i]); - if (type.comparator != null) { - value = type.comparator(value); - } - rowArray.push([value, row, position]); - } - rowArray.sort(compare); - for (_k = 0, _len2 = rowArray.length; _k < _len2; _k++) { - row = rowArray[_k]; - tBody.appendChild(row[1]); - } - } else { - _ref1 = tBody.rows; - for (_l = 0, _len3 = _ref1.length; _l < _len3; _l++) { - item = _ref1[_l]; - rowArray.push(item); - } - rowArray.reverse(); - for (_m = 0, _len4 = rowArray.length; _m < _len4; _m++) { - row = rowArray[_m]; - tBody.appendChild(row); - } - } - if (typeof window['CustomEvent'] === 'function') { - return typeof table.dispatchEvent === "function" ? table.dispatchEvent(new CustomEvent('Sortable.sorted', { - bubbles: true - })) : void 0; - } - }; - _results = []; - for (_i = 0, _len = clickEvents.length; _i < _len; _i++) { - eventName = clickEvents[_i]; - _results.push(addEventListener(th, eventName, onClick)); - } - return _results; - }, - getColumnType: function(table, i) { - var row, specified, text, type, _i, _j, _len, _len1, _ref, _ref1, _ref2; - specified = (_ref = table.querySelectorAll('th')[i]) != null ? _ref.getAttribute('data-sortable-type') : void 0; - if (specified != null) { - return sortable.typesObject[specified]; - } - _ref1 = table.tBodies[0].rows; - for (_i = 0, _len = _ref1.length; _i < _len; _i++) { - row = _ref1[_i]; - text = sortable.getNodeValue(row.cells[i]); - _ref2 = sortable.types; - for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { - type = _ref2[_j]; - if (type.match(text)) { - return type; - } - } - } - return sortable.typesObject.alpha; - }, - getNodeValue: function(node) { - var dataValue; - if (!node) { - return ''; - } - dataValue = node.getAttribute('data-value'); - if (dataValue !== null) { - return dataValue; - } - if (typeof node.innerText !== 'undefined') { - return node.innerText.replace(trimRegExp, ''); - } - return node.textContent.replace(trimRegExp, ''); - }, - setupTypes: function(types) { - var type, _i, _len, _results; - sortable.types = types; - sortable.typesObject = {}; - _results = []; - for (_i = 0, _len = types.length; _i < _len; _i++) { - type = types[_i]; - _results.push(sortable.typesObject[type.name] = type); - } - return _results; - } - }; - - sortable.setupTypes([ - { - name: 'numeric', - defaultSortDirection: 'descending', - match: function(a) { - return a.match(numberRegExp); - }, - comparator: function(a) { - return parseFloat(a.replace(/[^0-9.-]/g, ''), 10) || 0; - } - }, { - name: 'date', - defaultSortDirection: 'ascending', - reverse: true, - match: function(a) { - return !isNaN(Date.parse(a)); - }, - comparator: function(a) { - return Date.parse(a) || 0; - } - }, { - name: 'alpha', - defaultSortDirection: 'ascending', - match: function() { - return true; - }, - compare: function(a, b) { - return a.localeCompare(b); - } - } - ]); - - setTimeout(sortable.init, 0); - - if (typeof define === 'function' && define.amd) { - define(function() { - return sortable; - }); - } else if (typeof exports !== 'undefined') { - module.exports = sortable; - } else { - window.Sortable = sortable; - } - -}).call(this); diff --git a/data/web/json_api.php b/data/web/json_api.php index f0715466..66b46cfb 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -2,168 +2,242 @@ require_once 'inc/prerequisites.inc.php'; error_reporting(E_ALL); if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_username'])) { - if (isset($_GET['action'])) { - $action = $_GET['action']; + if (isset($_GET['action']) && isset($_GET['object'])) { + $action = filter_input(INPUT_GET, 'action', FILTER_SANITIZE_STRING); + $object = filter_input(INPUT_GET, 'object', FILTER_SANITIZE_STRING); switch ($action) { - case "domain_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $data[] = mailbox_get_domain_details($domain); - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - } - else { - echo '{}'; - } - break; - case "mailbox_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $mailboxes = mailbox_get_mailboxes($domain); - if (!empty($mailboxes)) { - foreach ($mailboxes as $mailbox) { - $data[] = mailbox_get_mailbox_details($mailbox); + case "domain": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $data[] = mailbox_get_domain_details($domain); + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_domain_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; } - else { - echo '{}'; - } - break; - case "resource_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $resources = mailbox_get_resources($domain); - if (!empty($resources)) { - foreach ($resources as $resource) { - $data[] = mailbox_get_resource_details($resource); + break; + case "mailbox": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $mailboxes = mailbox_get_mailboxes($domain); + if (!empty($mailboxes)) { + foreach ($mailboxes as $mailbox) { + $data[] = mailbox_get_mailbox_details($mailbox); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_mailbox_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_mailbox_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + } - else { - echo '{}'; - } - break; - case "domain_alias_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $alias_domains = mailbox_get_alias_domains($domain); - if (!empty($alias_domains)) { - foreach ($alias_domains as $alias_domain) { - $data[] = mailbox_get_alias_domain_details($alias_domain); + break; + case "resource": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $resources = mailbox_get_resources($domain); + if (!empty($resources)) { + foreach ($resources as $resource) { + $data[] = mailbox_get_resource_details($resource); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_resource_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_resource_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + } - else { - echo '{}'; - } - break; - case "alias_table_data": - $domains = array_merge(mailbox_get_domains(), mailbox_get_alias_domains()); - if (!empty($domains)) { - foreach ($domains as $domain) { - $aliases = mailbox_get_aliases($domain); - if (!empty($aliases)) { - foreach ($aliases as $alias) { - $data[] = mailbox_get_alias_details($alias); + break; + case "alias-domain": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $alias_domains = mailbox_get_alias_domains($domain); + if (!empty($alias_domains)) { + foreach ($alias_domains as $alias_domain) { + $data[] = mailbox_get_alias_domain_details($alias_domain); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_alias_domains($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_alias_domains($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; } - else { - echo '{}'; + break; + case "alias": + switch ($object) { + case "all": + $domains = array_merge(mailbox_get_domains(), mailbox_get_alias_domains()); + if (!empty($domains)) { + foreach ($domains as $domain) { + $aliases = mailbox_get_aliases($domain); + if (!empty($aliases)) { + foreach ($aliases as $alias) { + $data[] = mailbox_get_alias_details($alias); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_alias_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_alias_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; } - break; - case "get_mailbox_details": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; - $data = mailbox_get_mailbox_details($object); - if (!isset($data) || empty($data)) { - echo '{}'; + break; + case "domain-admin": + switch ($object) { + case "all": + $domain_admins = get_domain_admins(); + if (!empty($domain_admins)) { + foreach ($domain_admins as $domain_admin) { + $data[] = get_domain_admin_details($domain_admin); + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + } + else { + echo '{}'; + } + break; + + default: + $data = get_domain_admin_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(get_domain_admin_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; } - else { - echo json_encode(mailbox_get_mailbox_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - break; - case "get_domain_details": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; - $data = mailbox_get_domain_details($object); - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - break; - case "get_u2f_reg_challenge": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; - if ( - ($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") - && - ($_SESSION["mailcow_cc_username"] == $object) - ) { + break; + case "u2f-registration": + if (($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") && $_SESSION["mailcow_cc_username"] == $object) { $data = $u2f->getRegisterData(get_u2f_registrations($object)); list($req, $sigs) = $data; $_SESSION['regReq'] = json_encode($req); echo 'var req = ' . json_encode($req) . '; var sigs = ' . json_encode($sigs) . ';'; } else { - echo '{}'; + return; } - break; - case "get_u2f_auth_challenge": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; + break; + case "u2f-authentication": if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) { $reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object))); $_SESSION['authReq'] = $reqs; echo 'var req = ' . $reqs . ';'; } else { - echo '{}'; + return; } - break; + break; default: echo '{}'; - break; + break; } } } \ No newline at end of file diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index 160a306e..cd1b34b3 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -36,6 +36,7 @@ $lang['danger']['object_exists'] = 'Objekt %s existiert bereits'; $lang['danger']['domain_exists'] = 'Domain %s existiert bereits'; $lang['danger']['alias_goto_identical'] = 'Alias- und Ziel-Adresse dürfen nicht identisch sein'; $lang['danger']['aliasd_targetd_identical'] = 'Alias-Domain darf nicht gleich Ziel-Domain sein'; +$lang['danger']['maxquota_empty'] = 'Max. Speicherplatz pro Mailbox darf nicht 0 sein.'; $lang['success']['alias_added'] = 'Alias-Adresse(n) wurden angelegt'; $lang['success']['alias_modified'] = 'Änderungen an Alias %s wurden gespeichert'; $lang['success']['aliasd_modified'] = 'Änderungen an Alias-Domain %s wurden gespeichert'; @@ -70,6 +71,7 @@ $lang['danger']['is_spam_alias'] = '%s lautet bereits eine Spam-Alias-Adresse'; $lang['danger']['quota_not_0_not_numeric'] = 'Speicherplatz muss numerisch und >= 0 sein'; $lang['danger']['domain_not_found'] = 'Domain "%s" nicht gefunden.'; $lang['danger']['max_mailbox_exceeded'] = 'Anzahl an Mailboxen überschritten (%d von %d)'; +$lang['danger']['max_alias_exceeded'] = 'Anzahl an Alias-Adressen überschritten'; $lang['danger']['mailbox_quota_exceeded'] = 'Speicherplatz überschreitet das Limit (max. %d MiB)'; $lang['danger']['mailbox_quota_left_exceeded'] = 'Nicht genügend Speicherplatz vorhanden (Speicherplatz anwendbar: %d MiB)'; $lang['success']['mailbox_added'] = 'Mailbox %s wurde angelegt'; @@ -246,6 +248,7 @@ $lang['mailbox']['add_domain_alias'] = 'Domain-Alias hinzufügen'; $lang['mailbox']['add_mailbox'] = 'Mailbox hinzufügen'; $lang['mailbox']['add_resource'] = 'Ressource hinzufügen'; $lang['mailbox']['add_alias'] = 'Alias hinzufügen'; +$lang['mailbox']['empty'] = 'Keine Einträge vorhanden'; $lang['info']['no_action'] = 'Keine Aktion anwendbar'; @@ -448,4 +451,6 @@ $lang['admin']['site_not_found'] = 'Kann mailcow Site-Konfiguration nicht finden $lang['admin']['public_folder_empty'] = 'Public folder name must not be empty'; // NEEDS TRANSLATION $lang['admin']['set_rr_failed'] = 'Kann Postfix Restriktionen nicht setzen'; $lang['admin']['no_record'] = 'Kein Eintrag'; +$lang['admin']['filter_table'] = 'Tabelle Filtern'; +$lang['admin']['empty'] = 'Keine Einträge vorhanden'; ?> diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index 6b602faf..f46c9ac1 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -38,6 +38,7 @@ $lang['danger']['object_exists'] = "Object %s already exists"; $lang['danger']['domain_exists'] = "Domain %s already exists"; $lang['danger']['alias_goto_identical'] = "Alias and goto address must not be identical"; $lang['danger']['aliasd_targetd_identical'] = "Alias domain must not be equal to target domain"; +$lang['danger']['maxquota_empty'] = 'Max. quota per mailbox must not be 0.'; $lang['success']['alias_added'] = "Alias address/es has/have been added"; $lang['success']['alias_modified'] = "Changes to alias have been saved"; $lang['success']['aliasd_modified'] = "Changes to alias domain have been saved"; @@ -72,6 +73,7 @@ $lang['danger']['is_spam_alias'] = "%s is already known as a spam alias address" $lang['danger']['quota_not_0_not_numeric'] = "Quota must be numeric and >= 0"; $lang['danger']['domain_not_found'] = "Domain not found."; $lang['danger']['max_mailbox_exceeded'] = "Max. mailboxes exceeded (%d of %d)"; +$lang['danger']['max_alias_exceeded'] = 'Max. aliases exceeded'; $lang['danger']['mailbox_quota_exceeded'] = "Quota exceeds the domain limit (max. %d MiB)"; $lang['danger']['mailbox_quota_left_exceeded'] = "Not enough space left (space left: %d MiB)"; $lang['success']['mailbox_added'] = "Mailbox %s has been added"; @@ -249,6 +251,7 @@ $lang['mailbox']['add_mailbox'] = 'Add mailbox'; $lang['mailbox']['add_resource'] = 'Add resource'; $lang['mailbox']['add_alias'] = 'Add alias'; $lang['mailbox']['add_domain_record_first'] = 'Please add a domain first'; +$lang['mailbox']['empty'] = 'No results'; $lang['info']['no_action'] = 'No action applicable'; @@ -459,4 +462,6 @@ $lang['admin']['site_not_found'] = 'Cannot locate mailcow site configuration'; $lang['admin']['public_folder_empty'] = 'Public folder name must not be empty'; $lang['admin']['set_rr_failed'] = 'Cannot set Postfix restrictions'; $lang['admin']['no_record'] = 'No record'; +$lang['admin']['filter_table'] = 'Filter table'; +$lang['admin']['empty'] = 'No results'; ?> diff --git a/data/web/mailbox.php b/data/web/mailbox.php index bc8ca6d2..fe2cd803 100644 --- a/data/web/mailbox.php +++ b/data/web/mailbox.php @@ -5,33 +5,11 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm require_once "inc/header.inc.php"; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; ?> -
-

tbody>tr.footable-empty>td { endif; ?>
+

@@ -49,14 +28,15 @@ table.footable>tbody>tr.footable-empty>td {
+
-

+

@@ -65,14 +45,15 @@ table.footable>tbody>tr.footable-empty>td {
+
-

+

@@ -80,14 +61,15 @@ table.footable>tbody>tr.footable-empty>td {
+
-

+

@@ -100,10 +82,10 @@ table.footable>tbody>tr.footable-empty>td {
-

+

diff --git a/data/web/user.php b/data/web/user.php index 94f2a762..03e1a22b 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -405,7 +405,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
- +
@@ -416,7 +416,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' - + From 8b7e3c718d034c5a509c63439b4a0de27afb8bf2 Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 28 Mar 2017 11:51:31 +0200 Subject: [PATCH 005/108] API format changes --- data/conf/nginx/site.conf | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/data/conf/nginx/site.conf b/data/conf/nginx/site.conf index d724b4f2..a78c483f 100644 --- a/data/conf/nginx/site.conf +++ b/data/conf/nginx/site.conf @@ -18,6 +18,11 @@ server { access_log /var/log/nginx/access.log; root /web; + location /api/v1/ { + try_files $uri $uri/ /json_api.php?$args; + } + rewrite ^/api/v1/([^/]+)/([^/]+)/?$ /json_api.php?action=$1&object=$2? last; + location ^~ /.well-known/acme-challenge/ { allow all; default_type "text/plain"; @@ -166,6 +171,11 @@ server { access_log /var/log/nginx/access.log; root /web; + location /api/v1/ { + try_files $uri $uri/ /json_api.php?$args; + } + rewrite ^/api/v1/([^/]+)/([^/]+)/?$ /json_api.php?action=$1&object=$2? last; + location ^~ /.well-known/acme-challenge/ { allow all; default_type "text/plain"; From abcdf841cd0ff95a6d24d3977540bea490140d17 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 3 Apr 2017 19:55:13 +0200 Subject: [PATCH 006/108] Reset SQL users, logs: info about tail --- docs/u_and_e.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/u_and_e.md b/docs/u_and_e.md index d0a52cc8..5114d6a3 100644 --- a/docs/u_and_e.md +++ b/docs/u_and_e.md @@ -215,6 +215,51 @@ source mailcow.conf docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} < backup_file.sql ``` +### Reset MySQL passwords + +Stop the stack by running `docker-compose stop`. + +When the containers came to a stop, run this command: + +``` +docker-compose run --rm --entrypoint '/bin/sh -c "gosu mysql mysqld --skip-grant-tables & sleep 10 && mysql -hlocalhost -uroot && exit 0"' mysql-mailcow +``` + +**1\. Find database name** + +``` +MariaDB [(none)]> show databases; ++--------------------+ +| Database | ++--------------------+ +| information_schema | +| mailcow_database | <===== +| mysql | +| performance_schema | ++--------------------+ +4 rows in set (0.00 sec) +``` + +**2\. Reset one or more users + +Both "password" and "authentication_string" exist. Currently "password" is used, but better set both. + +``` +MariaDB [(none)]> SELECT user FROM mysql.user; ++--------------+ +| user | ++--------------+ +| mailcow_user | <===== +| root | ++--------------+ +2 rows in set (0.00 sec) + +MariaDB [(none)]> FLUSH PRIVILEGES; +MariaDB [(none)]> UPDATE mysql.user SET authentication_string = PASSWORD('gotr00t'), password = PASSWORD('gotr00t') WHERE User = 'root' AND Host = '%'; +MariaDB [(none)]> UPDATE mysql.user SET authentication_string = PASSWORD('mookuh'), password = PASSWORD('mookuh') WHERE User = 'mailcow' AND Host = '%'; +MariaDB [(none)]> FLUSH PRIVILEGES; +``` + ## Debugging You can use `docker-compose logs $service-name` for all containers. @@ -223,6 +268,8 @@ Run `docker-compose logs` for all logs at once. Follow the log output by running docker-compose with `logs -f`. +Limit the output by calling logs with `--tail=300` like `docker-compose logs --tail=300 mysql-mailcow`. + ## Redirect port 80 to 443 Since February the 28th 2017 mailcow does come with port 80 and 443 enabled. From 58d86dadcec43617d3fe58e998e958e4b3daf389 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 3 Apr 2017 20:06:49 +0200 Subject: [PATCH 007/108] Rebase Dovecot on Stretch slim, build from stable source with latest stable Pigeonhole for antispam replacement --- data/Dockerfiles/dovecot/Dockerfile | 86 +++++++++++-------- data/Dockerfiles/dovecot/docker-entrypoint.sh | 32 +++++-- data/Dockerfiles/dovecot/report-ham.sieve | 11 +++ data/Dockerfiles/dovecot/report-spam.sieve | 3 + data/Dockerfiles/dovecot/rspamd-pipe | 8 -- data/Dockerfiles/dovecot/rspamd-pipe-ham | 4 + data/Dockerfiles/dovecot/rspamd-pipe-spam | 4 + data/Dockerfiles/dovecot/supervisord.conf | 2 +- docker-compose.yml | 6 +- 9 files changed, 105 insertions(+), 51 deletions(-) create mode 100644 data/Dockerfiles/dovecot/report-ham.sieve create mode 100644 data/Dockerfiles/dovecot/report-spam.sieve delete mode 100755 data/Dockerfiles/dovecot/rspamd-pipe create mode 100755 data/Dockerfiles/dovecot/rspamd-pipe-ham create mode 100755 data/Dockerfiles/dovecot/rspamd-pipe-spam diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index abd07c1f..2d0578a2 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -1,33 +1,30 @@ -FROM ubuntu:xenial +FROM debian:stretch-slim +#ubuntu:xenial MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C +ENV DOVECOT_VERSION 2.2.28 +ENV PIGEONHOLE_VERSION 0.4.17 -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - -RUN apt-get update -RUN apt-get -y install dovecot-common \ - dovecot-core \ - dovecot-imapd \ - dovecot-lmtpd \ - dovecot-managesieved \ - dovecot-sieve \ - dovecot-mysql \ - dovecot-pop3d \ - dovecot-dev \ +RUN apt-get update \ + && apt-get -y install libpam-dev \ + default-libmysqlclient-dev \ + lzma-dev \ + liblz-dev \ + libbz2-dev \ + liblz4-dev \ + liblzma-dev \ + build-essential \ + autotools-dev \ + automake \ syslog-ng \ syslog-ng-core \ ca-certificates \ supervisor \ wget \ curl \ - build-essential \ - autotools-dev \ - automake \ + libssl-dev \ libauthen-ntlm-perl \ libcrypt-ssleay-perl \ libdigest-hmac-perl \ @@ -52,36 +49,57 @@ RUN apt-get -y install dovecot-common \ make \ cpanminus + +RUN wget https://www.dovecot.org/releases/2.2/dovecot-$DOVECOT_VERSION.tar.gz -O - | tar xvz \ + && cd dovecot-$DOVECOT_VERSION \ + && ./configure --with-mysql --with-lzma --with-lz4 --with-ssl=openssl --with-notify=inotify --with-storages=mdbox,sdbox,maildir,mbox,imapc,pop3c --with-bzlib --with-zlib \ + && make -j3 \ + && make install \ + && make clean + +RUN wget https://pigeonhole.dovecot.org/releases/2.2/dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION.tar.gz -O - | tar xvz \ + && cd dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION \ + && ./configure \ + && make -j3 \ + && make install \ + && make clean + RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf RUN cpanm Data::Uniqid Mail::IMAPClient String::Util RUN echo '* * * * * root /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync RUN echo '30 3 * * * vmail /usr/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync -WORKDIR /tmp - -RUN wget http://hg.dovecot.org/dovecot-antispam-plugin/archive/tip.tar.gz -O - | tar xvz \ - && cd /tmp/dovecot-antispam* \ - && ./autogen.sh \ - && ./configure --prefix=/usr \ - && make \ - && make install - COPY ./imapsync /usr/local/bin/imapsync COPY ./postlogin.sh /usr/local/bin/postlogin.sh COPY ./imapsync_cron.pl /usr/local/bin/imapsync_cron.pl -COPY ./rspamd-pipe /usr/local/bin/rspamd-pipe +COPY ./report-spam.sieve /usr/local/lib/dovecot/sieve/report-spam.sieve +COPY ./report-ham.sieve /usr/local/lib/dovecot/sieve/report-ham.sieve +COPY ./rspamd-pipe-ham /usr/local/lib/dovecot/sieve/rspamd-pipe-ham +COPY ./rspamd-pipe-spam /usr/local/lib/dovecot/sieve/rspamd-pipe-spam COPY ./docker-entrypoint.sh / COPY ./supervisord.conf /etc/supervisor/supervisord.conf -RUN chmod +x /usr/local/bin/rspamd-pipe -RUN chmod +x /usr/local/bin/imapsync_cron.pl +RUN chmod +x /usr/local/lib/dovecot/sieve/rspamd-pipe-ham \ + /usr/local/lib/dovecot/sieve/rspamd-pipe-spam \ + /usr/local/bin/imapsync_cron.pl \ + /usr/local/bin/postlogin.sh \ + /usr/local/bin/imapsync -RUN groupadd -g 5000 vmail -RUN useradd -g vmail -u 5000 vmail -d /var/vmail +RUN groupadd -g 5000 vmail \ + && groupadd -g 142 dovecot \ + && groupadd -g 143 dovenull \ + && useradd -g vmail -u 5000 vmail -d /var/vmail \ + && useradd -c "Dovecot unprivileged user" -d /dev/null -u 142 -g dovecot -s /bin/false dovecot \ + && useradd -c "Dovecot login user" -d /dev/null -u 143 -g dovenull -s /bin/false dovenull EXPOSE 24 10001 ENTRYPOINT ["/docker-entrypoint.sh"] CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf -RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +RUN apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + /tmp/* \ + /var/tmp/* \ + /dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION \ + /dovecot-$DOVECOT_VERSION diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 8ef09dba..282eb157 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -6,12 +6,16 @@ sed -i "/^\$DBUSER/c\\\$DBUSER='${DBUSER}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBPASS/c\\\$DBPASS='${DBPASS}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBNAME/c\\\$DBNAME='${DBNAME}';" /usr/local/bin/imapsync_cron.pl -[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/ +# Create SQL dict directory for Dovecot +[[ ! -d /usr/local/etc/dovecot/sql/ ]] && mkdir -p /usr/local/etc/dovecot/sql/ +[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve +[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo # Set Dovecot sql config parameters, escape " in db password DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g') -cat < /etc/dovecot/sql/dovecot-dict-sql.conf +# Create quota dict for Dovecot +cat < /usr/local/etc/dovecot/sql/dovecot-dict-sql.conf connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}" map { pattern = priv/quota/storage @@ -27,7 +31,8 @@ map { } EOF -cat < /etc/dovecot/sql/dovecot-mysql.conf +# Create user and pass dict for Dovecot +cat < /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = mysql connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}" default_pass_scheme = SSHA256 @@ -36,19 +41,32 @@ user_query = SELECT CONCAT('maildir:/var/vmail/',maildir) AS mail, 5000 AS uid, iterate_query = SELECT username FROM mailbox WHERE active='1'; EOF -[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve -[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo -cat /etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve +# Create global sieve_after script +cat /usr/local/etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve + +# Compile sieve scripts sievec /var/vmail/sieve/global.sieve +sievec /usr/local/lib/dovecot/sieve/report-spam.sieve +sievec /usr/local/lib/dovecot/sieve/report-ham.sieve + +# Fix sieve permission chown -R vmail:vmail /var/vmail/sieve +# Check permissions of vmail directory. # Do not do this every start-up, it may take a very long time. So we use a stat check here. if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi # Create random master for SOGo sieve features RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1) RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1) -echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /etc/dovecot/dovecot-master.passwd +echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /usr/local/etc/dovecot/dovecot-master.passwd echo ${RAND_USER}:${RAND_PASS} > /etc/sogo/sieve.creds +if [[ ! -f /mail_crypt/ecprivkey.pem || ! -f /mail_crypt/ecpubkey.pem ]]; then + openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem + openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem + chown -R dovecot -R /mail_crypt/ + chattr + /mail_crypt/ecpubkey.pem /mail_crypt/ecprivkey.pem +fi + exec "$@" diff --git a/data/Dockerfiles/dovecot/report-ham.sieve b/data/Dockerfiles/dovecot/report-ham.sieve new file mode 100644 index 00000000..80c7f44e --- /dev/null +++ b/data/Dockerfiles/dovecot/report-ham.sieve @@ -0,0 +1,11 @@ +require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; + +if environment :matches "imap.mailbox" "*" { + set "mailbox" "${1}"; +} + +if string "${mailbox}" "Trash" { + stop; +} + +pipe :copy "rspamd-pipe-ham"; diff --git a/data/Dockerfiles/dovecot/report-spam.sieve b/data/Dockerfiles/dovecot/report-spam.sieve new file mode 100644 index 00000000..d44cb9a7 --- /dev/null +++ b/data/Dockerfiles/dovecot/report-spam.sieve @@ -0,0 +1,3 @@ +require ["vnd.dovecot.pipe", "copy"]; + +pipe :copy "rspamd-pipe-spam"; diff --git a/data/Dockerfiles/dovecot/rspamd-pipe b/data/Dockerfiles/dovecot/rspamd-pipe deleted file mode 100755 index f9236e17..00000000 --- a/data/Dockerfiles/dovecot/rspamd-pipe +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -if [[ ${2} == "learn_spam" ]]; then -/usr/bin/curl --data-binary @- http://rspamd:11334/learnspam < /dev/stdin -elif [[ ${2} == "learn_ham" ]]; then -/usr/bin/curl --data-binary @- http://rspamd:11334/learnham < /dev/stdin -fi -# Always return 0 to satisfy Dovecot... -exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-ham b/data/Dockerfiles/dovecot/rspamd-pipe-ham new file mode 100755 index 00000000..7c3ab03f --- /dev/null +++ b/data/Dockerfiles/dovecot/rspamd-pipe-ham @@ -0,0 +1,4 @@ +#!/bin/bash +/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnham < /dev/stdin +# Always return 0 to satisfy Dovecot... +exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-spam b/data/Dockerfiles/dovecot/rspamd-pipe-spam new file mode 100755 index 00000000..67cccb2c --- /dev/null +++ b/data/Dockerfiles/dovecot/rspamd-pipe-spam @@ -0,0 +1,4 @@ +#!/bin/bash +/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnspam < /dev/stdin +# Always return 0 to satisfy Dovecot... +exit 0 diff --git a/data/Dockerfiles/dovecot/supervisord.conf b/data/Dockerfiles/dovecot/supervisord.conf index 45f9ddd5..e5a66f22 100644 --- a/data/Dockerfiles/dovecot/supervisord.conf +++ b/data/Dockerfiles/dovecot/supervisord.conf @@ -8,7 +8,7 @@ autostart=true stdout_syslog=true [program:dovecot] -command=/usr/sbin/dovecot -F +command=/usr/local/sbin/dovecot -F autorestart=true [program:logfiles] diff --git a/docker-compose.yml b/docker-compose.yml index 76ca1b6b..825bf9a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,7 @@ services: volumes: - mysql-vol-1:/var/lib/mysql/ - ./data/conf/mysql/:/etc/mysql/conf.d/:ro + - ./data/assets/reset_mysql.sh:/reset_mysql.sh dns: - 172.22.1.254 dns_search: mailcow-network @@ -151,14 +152,16 @@ services: depends_on: - bind9-mailcow volumes: - - ./data/conf/dovecot:/etc/dovecot + - ./data/conf/dovecot:/usr/local/etc/dovecot - ./data/assets/ssl:/etc/ssl/mail/:ro - ./data/conf/sogo/:/etc/sogo/ - vmail-vol-1:/var/vmail + - crypt-vol-1:/mail_crypt/ environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} - DBPASS=${DBPASS} + - MAIL_CRYPT=${MAIL_CRYPT:-NO} ports: - "${IMAP_PORT:-143}:143" - "${IMAPS_PORT:-993}:993" @@ -266,3 +269,4 @@ volumes: redis-vol-1: rspamd-vol-1: postfix-vol-1: + crypt-vol-1: From 34bc24255484698a69e1e35703841547cce8947f Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:19:01 +0200 Subject: [PATCH 008/108] Add Zeyple filter --- data/conf/postfix/main.cf | 1 + docker-compose.yml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index cbfc4e0c..46de4759 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -91,3 +91,4 @@ smtpd_milters = inet:rmilter:9900 non_smtpd_milters = inet:rmilter:9900 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} mydestination = localhost.localdomain, localhost +content_filter=zeyple diff --git a/docker-compose.yml b/docker-compose.yml index 825bf9a8..ca42c788 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -161,7 +161,6 @@ services: - DBNAME=${DBNAME} - DBUSER=${DBUSER} - DBPASS=${DBPASS} - - MAIL_CRYPT=${MAIL_CRYPT:-NO} ports: - "${IMAP_PORT:-143}:143" - "${IMAPS_PORT:-993}:993" From 2b955d08ab31e3aa16c02a596dd9982b80e672f4 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:19:41 +0200 Subject: [PATCH 009/108] Base on Stretch, add Zeyple --- data/Dockerfiles/postfix/Dockerfile | 13 +- data/Dockerfiles/postfix/postfix.sh | 14 +- data/Dockerfiles/postfix/supervisord.conf | 15 +- data/Dockerfiles/postfix/zeyple.conf | 9 + data/Dockerfiles/postfix/zeyple.py | 274 ++++++++++++++++++++++ 5 files changed, 319 insertions(+), 6 deletions(-) create mode 100644 data/Dockerfiles/postfix/zeyple.conf create mode 100755 data/Dockerfiles/postfix/zeyple.py diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 6a36f443..0fcdc893 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:xenial +FROM debian:testing-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive @@ -19,10 +19,19 @@ RUN apt-get install -y --no-install-recommends supervisor \ postfix-pcre \ syslog-ng \ syslog-ng-core \ - ca-certificates + ca-certificates \ + gnupg \ + python-gpgme \ + sudo \ + dirmngr +RUN addgroup --system --gid 600 zeyple +RUN adduser --system --home /var/lib/zeyple --no-create-home --uid 600 --gid 600 --disabled-login zeyple +RUN touch /var/log/zeyple.log && chown zeyple: /var/log/zeyple.log RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf +COPY zeyple.py /usr/local/bin/zeyple.py +COPY zeyple.conf /etc/zeyple.conf COPY supervisord.conf /etc/supervisor/supervisord.conf COPY postfix.sh /opt/postfix.sh diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index c628e771..f2ba03e4 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -17,7 +17,7 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', 'DUNNO') AS 'tls_enforce_in'; +query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', NULL) AS 'tls_enforce_in'; EOF cat < /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf @@ -25,7 +25,7 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', 'DUNNO') AS 'tls_enforce_out'; +query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', NULL) AS 'tls_enforce_out'; EOF cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf @@ -92,11 +92,21 @@ dbname = ${DBNAME} query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP() EOF +# Reset GPG key permissions +mkdir -p /var/lib/zeyple/keys +chmod 700 /var/lib/zeyple/keys +chown -R 600:600 /var/lib/zeyple/keys + +# Fix Postfix permissions +chgrp -R postdrop /var/spool/postfix/public +chgrp -R postdrop /var/spool/postfix/maildrop +postfix set-permissions postconf -c /opt/postfix/conf if [[ $? != 0 ]]; then echo "Postfix configuration error, refusing to start." exit 1 else postfix -c /opt/postfix/conf start + supervisorctl restart postfix-maillog sleep 126144000 fi diff --git a/data/Dockerfiles/postfix/supervisord.conf b/data/Dockerfiles/postfix/supervisord.conf index 4268899d..72523a61 100644 --- a/data/Dockerfiles/postfix/supervisord.conf +++ b/data/Dockerfiles/postfix/supervisord.conf @@ -12,6 +12,17 @@ command=/opt/postfix.sh autorestart=true [program:postfix-maillog] -command=/usr/bin/tail -f /var/log/mail.log -stdout_logfile=/dev/fd/1 +command=/bin/tail -f /var/log/zeyple.log /var/log/mail.log +stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 + +[unix_http_server] +file=/var/tmp/supervisord.sock +chmod=0770 +chown=nobody:nogroup + +[supervisorctl] +serverurl=unix:///var/tmp/supervisord.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/data/Dockerfiles/postfix/zeyple.conf b/data/Dockerfiles/postfix/zeyple.conf new file mode 100644 index 00000000..7f039582 --- /dev/null +++ b/data/Dockerfiles/postfix/zeyple.conf @@ -0,0 +1,9 @@ +[zeyple] +log_file = /var/log/zeyple.log + +[gpg] +home = /var/lib/zeyple/keys + +[relay] +host = localhost +port = 10026 diff --git a/data/Dockerfiles/postfix/zeyple.py b/data/Dockerfiles/postfix/zeyple.py new file mode 100755 index 00000000..bb218831 --- /dev/null +++ b/data/Dockerfiles/postfix/zeyple.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import os +import logging +import email +import email.mime.multipart +import email.mime.application +import email.encoders +import smtplib +import copy +from io import BytesIO + +try: + from configparser import SafeConfigParser # Python 3 +except ImportError: + from ConfigParser import SafeConfigParser # Python 2 + +import gpgme + +# Boiler plate to avoid dependency on six +# BBB: Python 2.7 support +PY3K = sys.version_info > (3, 0) + + +def message_from_binary(message): + if PY3K: + return email.message_from_bytes(message) + else: + return email.message_from_string(message) + + +def as_binary_string(email): + if PY3K: + return email.as_bytes() + else: + return email.as_string() + + +def encode_string(string): + if isinstance(string, bytes): + return string + else: + return string.encode('utf-8') + + +__title__ = 'Zeyple' +__version__ = '1.2.0' +__author__ = 'Cédric Félizard' +__license__ = 'AGPLv3+' +__copyright__ = 'Copyright 2012-2016 Cédric Félizard' + + +class Zeyple: + """Zeyple Encrypts Your Precious Log Emails""" + + def __init__(self, config_fname='zeyple.conf'): + self.config = self.load_configuration(config_fname) + + log_file = self.config.get('zeyple', 'log_file') + logging.basicConfig( + filename=log_file, level=logging.DEBUG, + format='%(asctime)s %(process)s %(levelname)s %(message)s' + ) + logging.info("Zeyple ready to encrypt outgoing emails") + + def load_configuration(self, filename): + """Reads and parses the config file""" + + config = SafeConfigParser() + config.read([ + os.path.join('/etc/', filename), + filename, + ]) + if not config.sections(): + raise IOError('Cannot open config file.') + return config + + @property + def gpg(self): + protocol = gpgme.PROTOCOL_OpenPGP + + if self.config.has_option('gpg', 'executable'): + executable = self.config.get('gpg', 'executable') + else: + executable = None # Default value + + home_dir = self.config.get('gpg', 'home') + + ctx = gpgme.Context() + ctx.set_engine_info(protocol, executable, home_dir) + ctx.armor = True + + return ctx + + def process_message(self, message_data, recipients): + """Encrypts the message with recipient keys""" + message_data = encode_string(message_data) + + in_message = message_from_binary(message_data) + logging.info( + "Processing outgoing message %s", in_message['Message-id']) + + if not recipients: + logging.warn("Cannot find any recipients, ignoring") + + sent_messages = [] + for recipient in recipients: + logging.info("Recipient: %s", recipient) + + key_id = self._user_key(recipient) + logging.info("Key ID: %s", key_id) + + if key_id: + out_message = self._encrypt_message(in_message, key_id) + + # Delete Content-Transfer-Encoding if present to default to + # "7bit" otherwise Thunderbird seems to hang in some cases. + del out_message["Content-Transfer-Encoding"] + else: + logging.warn("No keys found, message will be sent unencrypted") + out_message = copy.copy(in_message) + + self._add_zeyple_header(out_message) + self._send_message(out_message, recipient) + sent_messages.append(out_message) + + return sent_messages + + def _get_version_part(self): + ret = email.mime.application.MIMEApplication( + 'Version: 1\n', + 'pgp-encrypted', + email.encoders.encode_noop, + ) + ret.add_header( + 'Content-Description', + "PGP/MIME version identification", + ) + return ret + + def _get_encrypted_part(self, payload): + ret = email.mime.application.MIMEApplication( + payload, + 'octet-stream', + email.encoders.encode_noop, + name="encrypted.asc", + ) + ret.add_header('Content-Description', "OpenPGP encrypted message") + ret.add_header( + 'Content-Disposition', + 'inline', + filename='encrypted.asc', + ) + return ret + + def _encrypt_message(self, in_message, key_id): + if in_message.is_multipart(): + # get the body (after the first \n\n) + payload = in_message.as_string().split("\n\n", 1)[1].strip() + + # prepend the Content-Type including the boundary + content_type = "Content-Type: " + in_message["Content-Type"] + payload = content_type + "\n\n" + payload + + message = email.message.Message() + message.set_payload(payload) + + payload = message.get_payload() + + else: + payload = in_message.get_payload() + payload = encode_string(payload) + + quoted_printable = email.charset.Charset('ascii') + quoted_printable.body_encoding = email.charset.QP + + message = email.mime.nonmultipart.MIMENonMultipart( + 'text', 'plain', charset='utf-8' + ) + message.set_payload(payload, charset=quoted_printable) + + mixed = email.mime.multipart.MIMEMultipart( + 'mixed', + None, + [message], + ) + + # remove superfluous header + del mixed['MIME-Version'] + + payload = as_binary_string(mixed) + + encrypted_payload = self._encrypt_payload(payload, [key_id]) + + version = self._get_version_part() + encrypted = self._get_encrypted_part(encrypted_payload) + + out_message = copy.copy(in_message) + out_message.preamble = "This is an OpenPGP/MIME encrypted " \ + "message (RFC 4880 and 3156)" + + if 'Content-Type' not in out_message: + out_message['Content-Type'] = 'multipart/encrypted' + else: + out_message.replace_header( + 'Content-Type', + 'multipart/encrypted', + ) + + out_message.set_param('protocol', 'application/pgp-encrypted') + out_message.set_payload([version, encrypted]) + + return out_message + + def _encrypt_payload(self, payload, key_ids): + """Encrypts the payload with the given keys""" + payload = encode_string(payload) + + plaintext = BytesIO(payload) + ciphertext = BytesIO() + + self.gpg.armor = True + + recipient = [self.gpg.get_key(key_id) for key_id in key_ids] + + self.gpg.encrypt(recipient, gpgme.ENCRYPT_ALWAYS_TRUST, + plaintext, ciphertext) + + return ciphertext.getvalue() + + def _user_key(self, email): + """Returns the GPG key for the given email address""" + logging.info("Trying to encrypt for %s", email) + keys = [key for key in self.gpg.keylist(email)] + + if keys: + key = keys.pop() # NOTE: looks like keys[0] is the master key + key_id = key.subkeys[0].keyid + return key_id + + return None + + def _add_zeyple_header(self, message): + if self.config.has_option('zeyple', 'add_header') and \ + self.config.getboolean('zeyple', 'add_header'): + message.add_header( + 'X-Zeyple', + "processed by {0} v{1}".format(__title__, __version__) + ) + + def _send_message(self, message, recipient): + """Sends the given message through the SMTP relay""" + logging.info("Sending message %s", message['Message-id']) + + smtp = smtplib.SMTP(self.config.get('relay', 'host'), + self.config.get('relay', 'port')) + + smtp.sendmail(message['From'], recipient, message.as_string()) + smtp.quit() + + logging.info("Message %s sent", message['Message-id']) + + +if __name__ == '__main__': + recipients = sys.argv[1:] + + # BBB: Python 2.7 support + binary_stdin = sys.stdin.buffer if PY3K else sys.stdin + message = binary_stdin.read() + + zeyple = Zeyple() + zeyple.process_message(message, recipients) From 96c1a7c225aad56c710549e0c36eb5b0aa2275ea Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:21:20 +0200 Subject: [PATCH 010/108] Open 10026 for Zeyple --- data/conf/postfix/master.cf | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/data/conf/postfix/master.cf b/data/conf/postfix/master.cf index e48ea92e..3802c9a0 100644 --- a/data/conf/postfix/master.cf +++ b/data/conf/postfix/master.cf @@ -16,6 +16,7 @@ smtp_enforced_tls unix - - n - - smtp -o smtp_tls_security_level=encrypt -o syslog_name=enforced-tls-smtp -o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter + tlsproxy unix - - n - 0 tlsproxy dnsblog unix - - n - 0 dnsblog pickup fifo n - n 60 1 pickup @@ -43,3 +44,14 @@ anvil unix - - n - 1 anvil scache unix - - n - 1 scache maildrop unix - n n - - pipe flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient} +zeyple unix - n n - - pipe + user=zeyple argv=/usr/local/bin/zeyple.py ${recipient} +127.0.0.1:10026 inet n - n - 10 smtpd + -o content_filter= + -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters + -o smtpd_helo_restrictions= + -o smtpd_client_restrictions= + -o smtpd_sender_restrictions= + -o smtpd_recipient_restrictions=permit_mynetworks,reject + -o mynetworks=127.0.0.0/8 + -o smtpd_authorized_xforward_hosts=127.0.0.0/8 From b0d8b1344a8b623bd99755eb34e55ac76560cf85 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:22:23 +0200 Subject: [PATCH 011/108] Remove obsolete parameters, use imapsieve instead of deprecated antispam module, use mail_crypt per default --- data/conf/dovecot/dovecot.conf | 43 ++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 1d05571e..6792eb34 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -10,9 +10,9 @@ disable_plaintext_auth = yes login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k" mail_home = /var/vmail/%d/%n mail_location = maildir:~/ -mail_plugins = quota acl zlib antispam +mail_plugins = quota acl zlib mail_crypt auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ -ssl_protocols = !SSLv3 !SSLv2 +#ssl_protocols = !SSLv3 !SSLv2 ssl_prefer_server_ciphers = yes ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA ssl_options = no_compression @@ -24,12 +24,12 @@ auth_master_user_separator = * mail_prefetch_count = 30 passdb { driver = passwd-file - args = /etc/dovecot/dovecot-master.passwd + args = /usr/local/etc/dovecot/dovecot-master.passwd master = yes pass = yes } passdb { - args = /etc/dovecot/sql/dovecot-mysql.conf + args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = sql } namespace inbox { @@ -202,15 +202,15 @@ listen = *,[::] ssl_cert = Date: Wed, 5 Apr 2017 22:25:16 +0200 Subject: [PATCH 012/108] Run sievec after adding keys, use fixed IDs for users --- data/Dockerfiles/dovecot/docker-entrypoint.sh | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 282eb157..4a2876d7 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -6,7 +6,7 @@ sed -i "/^\$DBUSER/c\\\$DBUSER='${DBUSER}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBPASS/c\\\$DBPASS='${DBPASS}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBNAME/c\\\$DBNAME='${DBNAME}';" /usr/local/bin/imapsync_cron.pl -# Create SQL dict directory for Dovecot +# Create missing directories [[ ! -d /usr/local/etc/dovecot/sql/ ]] && mkdir -p /usr/local/etc/dovecot/sql/ [[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve [[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo @@ -44,14 +44,6 @@ EOF # Create global sieve_after script cat /usr/local/etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve -# Compile sieve scripts -sievec /var/vmail/sieve/global.sieve -sievec /usr/local/lib/dovecot/sieve/report-spam.sieve -sievec /usr/local/lib/dovecot/sieve/report-ham.sieve - -# Fix sieve permission -chown -R vmail:vmail /var/vmail/sieve - # Check permissions of vmail directory. # Do not do this every start-up, it may take a very long time. So we use a stat check here. if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi @@ -62,11 +54,22 @@ RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1) echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /usr/local/etc/dovecot/dovecot-master.passwd echo ${RAND_USER}:${RAND_PASS} > /etc/sogo/sieve.creds +# 401 is user dovecot if [[ ! -f /mail_crypt/ecprivkey.pem || ! -f /mail_crypt/ecpubkey.pem ]]; then openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem - chown -R dovecot -R /mail_crypt/ - chattr + /mail_crypt/ecpubkey.pem /mail_crypt/ecprivkey.pem + chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem +else + chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem fi +# Compile sieve scripts +sievec /var/vmail/sieve/global.sieve +sievec /usr/local/lib/dovecot/sieve/report-spam.sieve +sievec /usr/local/lib/dovecot/sieve/report-ham.sieve + +# Fix permissions +chown -R vmail:vmail /var/vmail/sieve + + exec "$@" From 5bee39dc952ede74f330f5d84400db9e0c9f3ec2 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:25:34 +0200 Subject: [PATCH 013/108] Use fixed IDs --- data/Dockerfiles/dovecot/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 2d0578a2..90f7352d 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -86,11 +86,11 @@ RUN chmod +x /usr/local/lib/dovecot/sieve/rspamd-pipe-ham \ /usr/local/bin/imapsync RUN groupadd -g 5000 vmail \ - && groupadd -g 142 dovecot \ - && groupadd -g 143 dovenull \ + && groupadd -g 401 dovecot \ + && groupadd -g 402 dovenull \ && useradd -g vmail -u 5000 vmail -d /var/vmail \ - && useradd -c "Dovecot unprivileged user" -d /dev/null -u 142 -g dovecot -s /bin/false dovecot \ - && useradd -c "Dovecot login user" -d /dev/null -u 143 -g dovenull -s /bin/false dovenull + && useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \ + && useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull EXPOSE 24 10001 From 8e3dceb512dc9c491043a9b324dcd1b114663f61 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:25:59 +0200 Subject: [PATCH 014/108] Rebase images on stretch --- data/Dockerfiles/rmilter/Dockerfile | 9 ++------- data/Dockerfiles/rspamd/Dockerfile | 9 ++------- data/Dockerfiles/sogo/Dockerfile | 18 ++++++------------ 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/data/Dockerfiles/rmilter/Dockerfile b/data/Dockerfiles/rmilter/Dockerfile index 364c78c1..1d5db5b0 100644 --- a/data/Dockerfiles/rmilter/Dockerfile +++ b/data/Dockerfiles/rmilter/Dockerfile @@ -1,16 +1,11 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \ - && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ + && echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update \ && apt-get --no-install-recommends -y --force-yes install rmilter cron syslog-ng syslog-ng-core supervisor diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 70a0bbab..46a97748 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -1,16 +1,11 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \ - && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ + && echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update \ && apt-get -y install rspamd ca-certificates python-pip diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 43960438..348231de 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -1,17 +1,12 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C ENV GOSU_VERSION 1.9 -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-get update \ - && apt-get install -y --no-install-recommends apt-transport-https \ + && apt-get install -y --no-install-recommends apt-transport-https gnupg \ ca-certificates \ wget \ syslog-ng \ @@ -29,8 +24,11 @@ RUN apt-get update \ && chmod +x /usr/local/bin/gosu \ && gosu nobody true +RUN mkdir /usr/share/doc/sogo +RUN touch /usr/share/doc/sogo/empty.sh + RUN apt-key adv --keyserver keys.gnupg.net --recv-key 0x810273C4 \ - && echo "deb http://packages.inverse.ca/SOGo/nightly/3/ubuntu/ xenial xenial" > /etc/apt/sources.list.d/sogo.list \ + && echo "deb http://packages.inverse.ca/SOGo/nightly/3/debian/ jessie jessie" > /etc/apt/sources.list.d/sogo.list \ && apt-get update \ && apt-get -y --force-yes install sogo sogo-activesync @@ -42,10 +40,6 @@ RUN echo '0 0 * * * sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/s COPY ./reconf-domains.sh / COPY supervisord.conf /etc/supervisor/supervisord.conf -#EXPOSE 20000 -#EXPOSE 9191 -#EXPOSE 9192 - CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* From 406a9ffc5bcb0282b9217200a1660bddb49bf993 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:26:56 +0200 Subject: [PATCH 015/108] Change hint for TLS enforced messaging --- data/web/lang/lang.de.php | 2 +- data/web/lang/lang.en.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index cd1b34b3..15325cd4 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -147,7 +147,7 @@ $lang['user']['spamfilter_default_score'] = 'Standardwert:'; $lang['user']['spamfilter_hint'] = 'Der erste Wert beschreibt den "low spam score", der zweite Wert den "high spam score".'; $lang['user']['spamfilter_table_domain_policy'] = "n.v. (Domainrichtlinie)"; -$lang['user']['tls_policy_warning'] = 'Vorsicht: Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.
Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.'; +$lang['user']['tls_policy_warning'] = 'Vorsicht: Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.
Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.
Diese Einstellung ist aktiv für die primäre Mailbox, für alle Alias-Adressen, die dieser Mailbox direkt zugeordnet sind (lediglich eine einzige Ziel-Adresse) und der Adressen, die sich aus Alias-Domains ergeben. Ausgeschlossen sind temporäre Aliasse ("Spam-Alias-Adressen"), Catch-All Alias-Adressen sowie Alias-Adressen mit mehreren Zielen.'; $lang['user']['tls_policy'] = 'Verschlüsselungsrichtlinie'; $lang['user']['tls_enforce_in'] = 'TLS eingehend erzwingen'; $lang['user']['tls_enforce_out'] = 'TLS ausgehend erzwingen'; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index f46c9ac1..121e37b7 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -149,7 +149,7 @@ $lang['user']['spamfilter_default_score'] = 'Default values:'; $lang['user']['spamfilter_hint'] = 'The first value describes the "low spam score", the second represents the "high spam score".'; $lang['user']['spamfilter_table_domain_policy'] = "n/a (domain policy)"; -$lang['user']['tls_policy_warning'] = 'Warning: If you decide to enforce encrypted mail transfer, you may lose emails.
Messages to not satisfy the policy will be bounced with a hard fail by the mail system.'; +$lang['user']['tls_policy_warning'] = 'Warning: If you decide to enforce encrypted mail transfer, you may lose emails.
Messages to not satisfy the policy will be bounced with a hard fail by the mail system.
This option applies to your primary email address (login name), all addresses derived from alias domains as well as alias addresses with only this single mailbox as target.'; $lang['user']['tls_policy'] = 'Encryption policy'; $lang['user']['tls_enforce_in'] = 'Enforce TLS incoming'; $lang['user']['tls_enforce_out'] = 'Enforce TLS outgoing'; From cc29cfa0d75d46116fb850b0eb3b3b280d902889 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:27:30 +0200 Subject: [PATCH 016/108] Add admin section css --- data/web/css/admin.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 data/web/css/admin.css diff --git a/data/web/css/admin.css b/data/web/css/admin.css new file mode 100644 index 00000000..61f23763 --- /dev/null +++ b/data/web/css/admin.css @@ -0,0 +1,13 @@ +table.footable>tbody>tr.footable-empty>td { + font-size:15px !important; + font-style:italic; +} +.pagination a { + text-decoration: none !important; +} +.panel panel-default { + overflow: visible !important; +} +.table-responsive { + overflow: visible !important; +} \ No newline at end of file From 6a9468c19127449155cadb1d4911cce00af8c4b6 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:27:39 +0200 Subject: [PATCH 017/108] Add admin section css --- data/web/inc/header.inc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index a7ec1c52..ede05bed 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -19,6 +19,7 @@ ' : null;?> +' : null;?> From e96dd7fd77edf6eb795d73e922eaad74a80fcc89 Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:27:57 +0200 Subject: [PATCH 018/108] Fix typo --- docs/u_and_e.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/u_and_e.md b/docs/u_and_e.md index 5114d6a3..3f52b05e 100644 --- a/docs/u_and_e.md +++ b/docs/u_and_e.md @@ -240,7 +240,7 @@ MariaDB [(none)]> show databases; 4 rows in set (0.00 sec) ``` -**2\. Reset one or more users +**2\. Reset one or more users** Both "password" and "authentication_string" exist. Currently "password" is used, but better set both. From 655f6e713845eefc84755de7e9d72d0d3294467f Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 5 Apr 2017 22:32:18 +0200 Subject: [PATCH 019/108] Add Zeyple volume to Postfix --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index ca42c788..8864489d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -186,6 +186,7 @@ services: - ./data/conf/postfix:/opt/postfix/conf - ./data/assets/ssl:/etc/ssl/mail/:ro - postfix-vol-1:/var/spool/postfix + - crypt-vol-1:/var/lib/zeyple environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} From b63adcab6690bb42e154f3424427dbf73f486d91 Mon Sep 17 00:00:00 2001 From: Emilien Devos Date: Thu, 6 Apr 2017 19:17:19 +0200 Subject: [PATCH 020/108] Warn the user about installing mailcow on Debian 8 --- docs/install.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/install.md b/docs/install.md index 7c383a63..bbffb21a 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,5 +1,7 @@ ## Install mailcow +**WARNING**: Please use Ubuntu 16.04 instead of Debian 8 or [switch to the kernel 4.9 from jessie backports](https://packages.debian.org/jessie-backports/linux-image-amd64) because there is a bug (kernel panic) with the kernel 3.16 when running docker containers with healthchecks! Full details here: [github.com/docker/docker/issues/30402](https://github.com/docker/docker/issues/30402) and [forum.mailcow.email/t/solved-mailcow-docker-causes-kernel-panic-edit/448](https://forum.mailcow.email/t/solved-mailcow-docker-causes-kernel-panic-edit/448) + You need Docker and Docker Compose. 1\. Learn how to install [Docker](https://docs.docker.com/engine/installation/linux/) and [Docker Compose](https://docs.docker.com/compose/install/). From 022739dcab47ccb6aaf242ec72ea974452a73174 Mon Sep 17 00:00:00 2001 From: andryyy Date: Sat, 8 Apr 2017 23:35:58 +0200 Subject: [PATCH 021/108] Remove reset mysql script --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8864489d..b3d18790 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,6 @@ services: volumes: - mysql-vol-1:/var/lib/mysql/ - ./data/conf/mysql/:/etc/mysql/conf.d/:ro - - ./data/assets/reset_mysql.sh:/reset_mysql.sh dns: - 172.22.1.254 dns_search: mailcow-network From e03136bf214f4f2fdc3050bfbadcd1e549567969 Mon Sep 17 00:00:00 2001 From: andryyy Date: Sat, 8 Apr 2017 23:36:46 +0200 Subject: [PATCH 022/108] Add description to command --- data/Dockerfiles/postfix/postfix.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index f2ba03e4..640538b0 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -101,7 +101,10 @@ chown -R 600:600 /var/lib/zeyple/keys chgrp -R postdrop /var/spool/postfix/public chgrp -R postdrop /var/spool/postfix/maildrop postfix set-permissions + +# Check Postfix configuration postconf -c /opt/postfix/conf + if [[ $? != 0 ]]; then echo "Postfix configuration error, refusing to start." exit 1 From 7339bd446ee92dc5c8c20c14e40eb0219ebd4bea Mon Sep 17 00:00:00 2001 From: andryyy Date: Sat, 8 Apr 2017 23:37:13 +0200 Subject: [PATCH 023/108] How to add a relayhost --- docs/first_steps.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/first_steps.md b/docs/first_steps.md index 21c418db..ab7876dc 100644 --- a/docs/first_steps.md +++ b/docs/first_steps.md @@ -136,6 +136,29 @@ server { } ``` +## Optional: Setup a relayhost + +Insert these lines to `data/conf/postfix/main.cf`. "relayhost" does already exist (empty), just change its value. +``` +relayhost = [your-relayhost]:587 +smtp_sasl_password_maps = hash:/opt/postfix/conf/smarthost_passwd +smtp_sasl_auth_enable = yes +``` + +Create the credentials file: +``` +echo "your-relayhost username:password" > data/conf/postfix/smarthost_passwd +``` + +Run: +``` +docker-compose exec postfix-mailcow postmap /opt/postfix/conf/smarthost_passwd +docker-compose exec postfix-mailcow chown root:postfix /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db +docker-compose exec postfix-mailcow chmod 660 /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db +docker-compose exec postfix-mailcow postfix reload +``` + + ## Install a local MTA The easiest option would be to disable the listener on port 25/tcp. From 7f8d6556b8a623cde9c1a322f1dbdff90cf5a405 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Apr 2017 13:07:39 +0200 Subject: [PATCH 024/108] imapsync: added option to delete from source after transfer --- data/conf/dovecot/dovecot.conf | 3 +-- data/web/add.php | 7 +++++++ data/web/edit.php | 14 ++++++++++++++ data/web/inc/functions.inc.php | 15 ++++++++++++--- data/web/lang/lang.de.php | 3 +++ data/web/lang/lang.en.php | 3 +++ data/web/lang/lang.ru.php | 1 + 7 files changed, 41 insertions(+), 5 deletions(-) diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 6792eb34..f90c6d50 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -11,8 +11,7 @@ login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k" mail_home = /var/vmail/%d/%n mail_location = maildir:~/ mail_plugins = quota acl zlib mail_crypt -auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ -#ssl_protocols = !SSLv3 !SSLv2 +ssl_protocols = !SSLv3 ssl_prefer_server_ciphers = yes ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA ssl_options = no_compression diff --git a/data/web/add.php b/data/web/add.php index 9361d14b..39017e98 100644 --- a/data/web/add.php +++ b/data/web/add.php @@ -350,6 +350,13 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == +
+
+
+ +
+
+
diff --git a/data/web/edit.php b/data/web/edit.php index 5879df6d..3f3311d0 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -620,6 +620,20 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] ==
+
+
+
+ +
+
+
+
+
+
+ +
+
+
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index cbc71102..465ab2ac 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -109,6 +109,11 @@ function init_db_schema() { if ($num_results == 0) { $pdo->query("ALTER TABLE `mailbox` ADD `multiple_bookings` tinyint(1) NOT NULL DEFAULT '0'"); } + $stmt = $pdo->query("SHOW COLUMNS FROM `imapsync` LIKE 'delete1'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results == 0) { + $pdo->query("ALTER TABLE `imapsync` ADD `delete1` tinyint(1) NOT NULL DEFAULT '0'"); + } $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'wants_tagged_subject'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); if ($num_results == 0) { @@ -1075,6 +1080,7 @@ function add_syncjob($postarray) { } isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0'; + isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0'; $port1 = $postarray['port1']; $host1 = $postarray['host1']; $password1 = $postarray['password1']; @@ -1147,12 +1153,13 @@ function add_syncjob($postarray) { return false; } try { - $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`) - VALUES (:user2, :exclude, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)"); + $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`) + VALUES (:user2, :exclude, :maxage, :delete1, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)"); $stmt->execute(array( ':user2' => $username, ':exclude' => $exclude, ':maxage' => $maxage, + ':delete1' => $delete1, ':subfolder2' => $subfolder2, ':host1' => $host1, ':authmech1' => 'PLAIN', @@ -1200,6 +1207,7 @@ function edit_syncjob($postarray) { } isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0'; + isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0'; $id = $postarray['id']; $port1 = $postarray['port1']; $host1 = $postarray['host1']; @@ -1273,10 +1281,11 @@ function edit_syncjob($postarray) { return false; } try { - $stmt = $pdo->prepare("UPDATE `imapsync` set `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active + $stmt = $pdo->prepare("UPDATE `imapsync` set `delete1` = :delete1, `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active WHERE `user2` = :user2 AND `id` = :id"); $stmt->execute(array( ':user2' => $username, + ':delete1' => $delete1, ':id' => $id, ':exclude' => $exclude, ':maxage' => $maxage, diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index b55ad03f..07d23d70 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -329,6 +329,9 @@ $lang['add']['subfolder2'] = 'Sync into subfolder on destination'; $lang['add']['mins_interval'] = 'Abrufintervall (Minuten)'; $lang['add']['exclude'] = 'Elemente ausschließen (Regex)'; $lang['add']['delete2duplicates'] = 'Lösche Duplikate im Ziel'; +$lang['add']['delete1'] = 'Lösche Nachricht nach Übertragung vom Quell-Server'; +$lang['edit']['delete2duplicates'] = 'Lösche Duplikate im Ziel'; +$lang['edit']['delete1'] = 'Lösche Nachricht nach Übertragung vom Quell-Server'; $lang['add']['title'] = 'Objekt anlegen'; $lang['add']['domain'] = 'Domain'; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index 05e37698..978ee713 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -333,6 +333,9 @@ $lang['add']['maxage'] = 'Maximum age of messages that will be polled from remot $lang['add']['subfolder2'] = 'Sync into subfolder on destination'; $lang['add']['exclude'] = 'Exclude objects (regex)'; $lang['add']['delete2duplicates'] = 'Delete duplicates on destination'; +$lang['add']['delete1'] = 'Delete from source when completed'; +$lang['edit']['delete2duplicates'] = 'Delete duplicates on destination'; +$lang['edit']['delete1'] = 'Delete from source when completed'; $lang['add']['title'] = 'Add object'; $lang['add']['domain'] = 'Domain'; diff --git a/data/web/lang/lang.ru.php b/data/web/lang/lang.ru.php index e2502b83..d07bbc9f 100644 --- a/data/web/lang/lang.ru.php +++ b/data/web/lang/lang.ru.php @@ -321,6 +321,7 @@ $lang['add']['maxage'] = 'Maximum age of messages that will be polled from remot $lang['add']['subfolder2'] = "Синхронизировать в подпапку по назначению"; $lang['add']['exclude'] = "Исключить объекты (regex)"; $lang['add']['delete2duplicates'] = "Удалить дубликаты в получателях"; +$lang['edit']['delete2duplicates'] = "Удалить дубликаты в получателях"; $lang['add']['title'] = "Добавить объект"; $lang['add']['domain'] = "Домен"; $lang['add']['active'] = "Активный"; From 06928ec84109ecb980d811d494be003def9ec07a Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Apr 2017 13:08:02 +0200 Subject: [PATCH 025/108] imapsync: added option to delete from source after transfer --- data/Dockerfiles/dovecot/imapsync_cron.pl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/data/Dockerfiles/dovecot/imapsync_cron.pl b/data/Dockerfiles/dovecot/imapsync_cron.pl index 5c47eb47..a3cbf8a1 100755 --- a/data/Dockerfiles/dovecot/imapsync_cron.pl +++ b/data/Dockerfiles/dovecot/imapsync_cron.pl @@ -21,7 +21,7 @@ open my $file, '<', "/etc/sogo/sieve.creds"; my $creds = <$file>; close $file; my ($master_user, $master_pass) = split /:/, $creds; -my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)"); +my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2, delete1 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)"); $sth->execute(); my $row; @@ -39,6 +39,7 @@ while ($row = $sth->fetchrow_arrayref()) { $delete2duplicates = @$row[9]; $maxage = @$row[10]; $subfolder2 = @$row[11]; + $delete1 = @$row[12]; if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; } @@ -46,11 +47,12 @@ while ($row = $sth->fetchrow_arrayref()) { "--timeout1", "10", "--tmpdir", "/tmp", "--subscribeall", - ($exclude eq "" ? () : ("--exclude", $exclude)), - ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), - ($maxage eq "0" ? () : ('--maxage', $maxage)), + ($exclude eq "" ? () : ("--exclude", $exclude)), + ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), + ($maxage eq "0" ? () : ('--maxage', $maxage)), ($delete2duplicates ne "1" ? () : ('--delete2duplicates')), - (!defined($enc1) ? () : ($enc1)), + ($delete1 ne "1" ? () : ('--delete')), + (!defined($enc1) ? () : ($enc1)), "--host1", $host1, "--user1", $user1, "--password1", $password1, From c460636a702a7d1176c7d1c3c39eda0edf74180e Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Apr 2017 13:09:33 +0200 Subject: [PATCH 026/108] mail_crypt is not enabled by default --- data/conf/dovecot/dovecot.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index f90c6d50..b4c2287a 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -239,9 +239,9 @@ plugin { sieve_max_script_size = 1M sieve_quota_max_scripts = 0 sieve_quota_max_storage = 0 - mail_crypt_global_private_key = Date: Mon, 10 Apr 2017 13:15:48 +0200 Subject: [PATCH 027/108] mail_crypt is not enabled by default --- data/conf/dovecot/dovecot.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index b4c2287a..b4501e1a 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -10,7 +10,7 @@ disable_plaintext_auth = yes login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k" mail_home = /var/vmail/%d/%n mail_location = maildir:~/ -mail_plugins = quota acl zlib mail_crypt +mail_plugins = quota acl zlib #mail_crypt ssl_protocols = !SSLv3 ssl_prefer_server_ciphers = yes ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA @@ -205,10 +205,10 @@ userdb { driver = sql } protocol imap { - mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt + mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve #mail_crypt } protocol lmtp { - mail_plugins = quota sieve acl zlib mail_crypt + mail_plugins = quota sieve acl zlib #mail_crypt auth_socket_path = /usr/local/var/run/dovecot/auth-master } protocol sieve { From d0d87ead49332a32b022c4540e0e53c41715da4e Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 10 Apr 2017 13:16:40 +0200 Subject: [PATCH 028/108] Zeyple is not enabled by default --- data/conf/postfix/main.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index 46de4759..b28f6eb9 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -91,4 +91,4 @@ smtpd_milters = inet:rmilter:9900 non_smtpd_milters = inet:rmilter:9900 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} mydestination = localhost.localdomain, localhost -content_filter=zeyple +#content_filter=zeyple From 76720f3e947384816769696cf251eac6d1361d22 Mon Sep 17 00:00:00 2001 From: Michael Kuron Date: Mon, 17 Apr 2017 13:16:54 +0200 Subject: [PATCH 029/108] Forwarding hosts in web interface --- data/web/admin.php | 68 ++++++++++++++++++++++++++++++++++ data/web/delete.php | 17 +++++++++ data/web/inc/functions.inc.php | 67 +++++++++++++++++++++++++++++++++ data/web/inc/init.sql | 5 +++ data/web/inc/triggers.inc.php | 6 +++ data/web/lang/lang.en.php | 6 +++ 6 files changed, 169 insertions(+) diff --git a/data/web/admin.php b/data/web/admin.php index e0e04aee..6d0bdf82 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -183,6 +183,8 @@ $tfa_data = get_tfa();

+
+
@@ -298,6 +300,72 @@ $tfa_data = get_tfa();
+ +
+
+ +
+
+
+

+ +
+
Server:Port Log
+ + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
-getRegisterData(getRegs($username)); - list($req, $sigs) = $data; - $_SESSION['regReq'] = json_encode($req); -?> - -getMessage(); - } - break; - - case 'authenticate': - try { - $reqs = json_encode($u2f->getAuthenticateData(getRegs($username))); - $_SESSION['authReq'] = $reqs; -?> - -getMessage(); - } - break; - } - } - if (!empty($_POST['u2f_register_data'])) { - try { - $reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($_POST['u2f_register_data'])); - addReg($username, $reg); - } - catch (Exception $e) { - echo "U2F error: " . $e->getMessage(); - } - finally { - echo "Success"; - $_SESSION['regReq'] = null; - } - } - if (!empty($_POST['u2f_auth_data'])) { - try { - $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), getRegs($username), json_decode($_POST['u2f_auth_data'])); - updateReg($reg); - } - catch (Exception $e) { - echo "U2F error: " . $e->getMessage(); - } - finally { - echo "Success"; - $_SESSION['authReq'] = null; - } - } - } -?> - - -
-
- - -
-
-
- -
-Username:

-Action:
- Register
- Authenticate
- -
- - - From f4a0289e7119b3c82e678f759eace9f4746b2f0e Mon Sep 17 00:00:00 2001 From: andryyy Date: Sat, 29 Apr 2017 22:19:35 +0200 Subject: [PATCH 087/108] Important fix: Adding a domain was not possible --- data/web/js/add.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/js/add.js b/data/web/js/add.js index e7a3fd90..05171d10 100644 --- a/data/web/js/add.js +++ b/data/web/js/add.js @@ -3,7 +3,7 @@ $(document).ready(function() { // Get max. possible quota for a domain when domain field changes $('#addSelectDomain').on('change', function() { $.get("/api/v1/get/domain/" + this.value, function(data){ - var result = jQuery.parseJSON( data ); + var result = $.parseJSON(JSON.stringify(data)); max_new_mailbox_quota = ( result.max_new_mailbox_quota / 1048576); if (max_new_mailbox_quota != '0') { $("#quotaBadge").html('max. ' + max_new_mailbox_quota + ' MiB'); From 3ffc20e23816289ae3d4fe069b41bbc6352ffc4e Mon Sep 17 00:00:00 2001 From: andryyy Date: Sun, 30 Apr 2017 12:18:19 +0200 Subject: [PATCH 088/108] Fixes #225 > Subquery can return multiple rows --- data/web/inc/functions.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 091b3623..12fd08c5 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -4294,7 +4294,7 @@ function mailbox_get_domain_details($domain) { $domaindata['relay_all_recipients_int'] = $row['relay_all_recipients_int']; $stmt = $pdo->prepare("SELECT COUNT(*) AS `alias_count` FROM `alias` - WHERE (`domain`= :domain OR `domain` = (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain2)) + WHERE (`domain`= :domain OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain2)) AND `address` NOT IN ( SELECT `username` FROM `mailbox` )"); From 53c3c470496c9726deee8225004391f5b56abe6e Mon Sep 17 00:00:00 2001 From: andryyy Date: Sun, 30 Apr 2017 21:44:42 +0200 Subject: [PATCH 089/108] Show mailcow apps in header --- data/web/inc/header.inc.php | 16 ++++++++++++++-- data/web/index.php | 8 +++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index ede05bed..678c2590 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -56,7 +56,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { ?>
From 4cce211c2200db72593dbd2fd184a47c4bc9db4f Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 1 May 2017 16:20:21 +0200 Subject: [PATCH 090/108] Fix db for sync jobs, fixes #232 --- data/web/inc/init_db.inc.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 3064bd8a..ed32b7e7 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "23042017_1807"; + $db_version = "01052017_1619"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -231,6 +231,7 @@ function init_db_schema() { "port1" => "SMALLINT NOT NULL", "enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'", "delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'", + "delete1" => "TINYINT(1) NOT NULL DEFAULT '0'", "returned_text" => "TEXT", "last_run" => "TIMESTAMP NULL DEFAULT NULL", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", From ea77f60dae33681c1257ed63aab5666048805c17 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 1 May 2017 16:34:09 +0200 Subject: [PATCH 091/108] Fixes U2F, fixes #231 --- data/web/inc/footer.inc.php | 4 ++-- data/web/json_api.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 6cf48242..3a3f9009 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -67,7 +67,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "/api/v1/u2f-authentication/", + url: "/api/v1/get/u2f-authentication/", success: function(data){ data; } @@ -100,7 +100,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "/api/v1/u2f-registration/", + url: "/api/v1/get/u2f-registration/", success: function(data){ data; } diff --git a/data/web/json_api.php b/data/web/json_api.php index 44e7d934..894a9442 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -235,6 +235,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u } break; case "u2f-registration": + header('Content-Type: application/javascript'); if (($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") && $_SESSION["mailcow_cc_username"] == $object) { $data = $u2f->getRegisterData(get_u2f_registrations($object)); list($req, $sigs) = $data; @@ -246,6 +247,7 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u } break; case "u2f-authentication": + header('Content-Type: application/javascript'); if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) { $reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object))); $_SESSION['authReq'] = $reqs; From e7a1ec227866c3ed0bc3c43ece43a7521bb85af5 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 1 May 2017 17:03:47 +0200 Subject: [PATCH 092/108] Fix key_id --- data/web/inc/init_db.inc.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index ed32b7e7..d856f26b 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "01052017_1619"; + $db_version = "01052017_1702"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -248,6 +248,7 @@ function init_db_schema() { "tfa" => array( "cols" => array( "id" => "INT NOT NULL AUTO_INCREMENT", + "key_id" => "VARCHAR(255) NOT NULL", "username" => "VARCHAR(255) NOT NULL", "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp')", "secret" => "VARCHAR(255) DEFAULT NULL", From eb6bc9e7386f46fcd3a9c2eeda3a61a0b75062ff Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 1 May 2017 19:13:46 +0200 Subject: [PATCH 093/108] Mailbox table: break all --- data/web/js/mailbox.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js index 0f0b4098..6119cdee 100644 --- a/data/web/js/mailbox.js +++ b/data/web/js/mailbox.js @@ -107,8 +107,8 @@ $(document).ready(function() { }); $('#mailbox_table').footable({ "columns": [ - {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}}, - {"name":"name","title":lang.fname,"breakpoints":"xs sm"}, + {"sorted": true,"name":"username","style":{"word-break":"break-all"},"title":lang.username}, + {"name":"name","title":lang.fname,"style":{"word-break":"break-all"},"breakpoints":"xs sm"}, {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota,"formatter": function(value){ res = value.split("/"); From 748e7b65e4a317c1f66b408d8bde0390a1825892 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 1 May 2017 21:24:55 +0200 Subject: [PATCH 094/108] Move docs to own repo --- docs/first_steps.md | 301 ------------------------ docs/images/logo.svg | 179 --------------- docs/index.md | 53 ----- docs/install.md | 126 ---------- docs/u_and_e.md | 536 ------------------------------------------- 5 files changed, 1195 deletions(-) delete mode 100644 docs/first_steps.md delete mode 100644 docs/images/logo.svg delete mode 100644 docs/index.md delete mode 100644 docs/install.md delete mode 100644 docs/u_and_e.md diff --git a/docs/first_steps.md b/docs/first_steps.md deleted file mode 100644 index cba908e5..00000000 --- a/docs/first_steps.md +++ /dev/null @@ -1,301 +0,0 @@ -## SSL (and: How to use Let's Encrypt) - -mailcow dockerized comes with a snakeoil CA "mailcow" and a server certificate in `data/assets/ssl`. Please use your own trusted certificates. - -mailcow uses 3 domain names that should be covered by your new certificate: - -- ${MAILCOW_HOSTNAME} -- autodiscover.**example.org** -- autoconfig.**example.org** - -### Obtain multi-SAN certificate by Let's Encrypt - -This is just an example of how to obtain certificates with certbot. There are several methods! - -1\. Get the certbot client: -``` bash -wget https://dl.eff.org/certbot-auto -O /usr/local/sbin/certbot && chmod +x /usr/local/sbin/certbot -``` - -2\. Make sure you set `HTTP_BIND=0.0.0.0` and `HTTP_PORT=80` in `mailcow.conf` or setup a reverse proxy to enable connections to port 80. If you changed HTTP_BIND, then rebuild Nginx: -``` bash -docker-compose up -d -``` - -3\. Request the certificate with the webroot method: -``` bash -cd /path/to/git/clone/mailcow-dockerized -source mailcow.conf -certbot certonly \ - --webroot \ - -w ${PWD}/data/web \ - -d ${MAILCOW_HOSTNAME} \ - -d autodiscover.example.org \ - -d autoconfig.example.org \ - --email you@example.org \ - --agree-tos -``` - -**Remember to replace the example.org domain with your own domain, this command will not work if you dont.** - -4\. Create hard links to the full path of the new certificates. Assuming you are still in the mailcow root folder: -``` bash -mv data/assets/ssl/cert.{pem,pem.backup} -mv data/assets/ssl/key.{pem,pem.backup} -ln $(readlink -f /etc/letsencrypt/live/${MAILCOW_HOSTNAME}/fullchain.pem) data/assets/ssl/cert.pem -ln $(readlink -f /etc/letsencrypt/live/${MAILCOW_HOSTNAME}/privkey.pem) data/assets/ssl/key.pem -``` - -5\. Restart affected containers: -``` -docker-compose restart postfix-mailcow dovecot-mailcow nginx-mailcow -``` - -When renewing certificates, run the last two steps (link + restart) as post-hook in a script. - -## Rspamd Web UI -At first you may want to setup Rspamds web interface which provides some useful features and information. - -1\. Generate a Rspamd controller password hash: -``` -docker-compose exec rspamd-mailcow rspamadm pw -``` - -2\. Replace the default hash in `data/conf/rspamd/override.d/worker-controller.inc` by your newly generated: -``` -enable_password = "myhash"; -``` - -You can use `password = "myhash";` instead of `enable_password` to disable write-access in the web UI. - -3\. Restart rspamd: -``` -docker-compose restart rspamd-mailcow -``` - -Open https://${MAILCOW_HOSTNAME}/rspamd in a browser and login! - -## Optional: Reverse proxy - -You don't need to change the Nginx site that comes with mailcow: dockerized. -mailcow: dockerized trusts the default gateway IP 172.22.1.1 as proxy. This is very important to control access to Rspamd's web UI. - -1\. Make sure you change HTTP_BIND and HTTPS_BIND in `mailcow.conf` to a local address and set the ports accordingly, for example: -``` bash -HTTP_BIND=127.0.0.1 -HTTP_PORT=8080 -HTTPS_PORT=127.0.0.1 -HTTPS_PORT=8443 -``` -** IMPORTANT: Do not use port 8081 ** - -Recreate affected containers by running `docker-compose up -d`. - -2\. Configure your local webserver as reverse proxy: - -### Apache 2.4 -``` apache - - ServerName mail.example.org - ServerAlias autodiscover.example.org - ServerAlias autoconfig.example.org - - [...] - # You should proxy to a plain HTTP session to offload SSL processing - ProxyPass / http://127.0.0.1:8080/ - ProxyPassReverse / http://127.0.0.1:8080/ - ProxyPreserveHost Off - your-ssl-configuration-here - [...] - - # If you plan to proxy to a HTTPS host: - #SSLProxyEngine On - - # If you plan to proxy to an untrusted HTTPS host: - #SSLProxyVerify none - #SSLProxyCheckPeerCN off - #SSLProxyCheckPeerName off - #SSLProxyCheckPeerExpire off - -``` - -### Nginx -``` -server { - listen 443; - server_name mail.example.org autodiscover.example.org autoconfig.example.org; - - [...] - your-ssl-configuration-here - location / { - proxy_pass http://127.0.0.1:8080/; - proxy_redirect http://127.0.0.1:8080/ $scheme://$host:$server_port/; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - [...] -} -``` - -## Optional: Setup a relayhost - -Insert these lines to `data/conf/postfix/main.cf`. "relayhost" does already exist (empty), just change its value. -``` -relayhost = [your-relayhost]:587 -smtp_sasl_password_maps = hash:/opt/postfix/conf/smarthost_passwd -smtp_sasl_auth_enable = yes -``` - -Create the credentials file: -``` -echo "your-relayhost username:password" > data/conf/postfix/smarthost_passwd -``` - -Run: -``` -docker-compose exec postfix-mailcow postmap /opt/postfix/conf/smarthost_passwd -docker-compose exec postfix-mailcow chown root:postfix /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db -docker-compose exec postfix-mailcow chmod 660 /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db -docker-compose exec postfix-mailcow postfix reload -``` - -### Helper script - -There is a helper script `mailcow-setup-relayhost.sh` you can run to setup a relayhost. - -``` bash -Usage: - -Setup a relayhost: -./mailcow-setup-relayhost.sh relayhost port (username) (password) -Username and password are optional parameters. - -Reset to defaults: -./mailcow-setup-relayhost.sh reset -``` - -## Optional: Log to Syslog - -Enable Rsyslog to receive logs on 524/tcp: - -``` -# This setting depends on your Rsyslog version and configuration format. -# For most Debian derivates it will work like this... -$ModLoad imtcp -$TCPServerAddress 127.0.0.1 -$InputTCPServerRun 524 - -# ...while for Ubuntu 16.04 it looks like this: -module(load="imtcp") -input(type="imtcp" address="127.0.0.1" port="524") - -# No matter your Rsyslog version, you should set this option to off -# if you plan to use Fail2ban -$RepeatedMsgReduction off -``` - -Restart rsyslog after enabling the TCP listener. - -Now setup Docker daemon to start with the syslog driver. -This enables the syslog driver for all containers! - -Debian users can change the startup configuration in `/etc/default/docker` while CentOS users find it in `/etc/sysconfig/docker`: -``` -... -DOCKER_OPTS="--log-driver=syslog --log-opt syslog-address=tcp://127.0.0.1:524" -... -``` - -**Caution:** For some reason Ubuntu 16.04 and some, but not all, systemd based distros do not read the defaults file parameters. - -Just run `systemctl edit docker.service` and add the following content to fix it. - -**Note:** If "systemctl edit" is not available, just copy the content to `/etc/systemd/system/docker.service.d/override.conf`. - -The first empty ExecStart parameter is not a mistake. - -``` -[Service] -EnvironmentFile=/etc/default/docker -ExecStart= -ExecStart=/usr/bin/docker daemon -H fd:// $DOCKER_OPTS -``` - -Restart the Docker daemon and run `docker-compose down && docker-compose up -d` to recreate the containers. - -### Use Fail2ban - -**This is a subsection of "Log to Syslog", which is required for Fail2ban to work.** - -Open `/etc/fail2ban/filter.d/common.conf` and search for the prefix_line parameter, change it to ".*": - -``` -__prefix_line = .* -``` - -Create `/etc/fail2ban/jail.d/dovecot.conf`... -``` -[dovecot] -enabled = true -filter = dovecot -logpath = /var/log/syslog -chain = FORWARD -``` - -and `jail.d/postfix-sasl.conf`: -``` -[postfix-sasl] -enabled = true -filter = postfix-sasl -logpath = /var/log/syslog -chain = FORWARD -``` - -Restart Fail2ban. - -## Install a local MTA - -The easiest option would be to disable the listener on port 25/tcp. - -**Postfix** users disable the listener by commenting the following line (starting with `smtp` or `25`) in `/etc/postfix/master.cf`: -``` -#smtp inet n - - - - smtpd -``` -Restart Postfix after applying your changes. - -## Sender and receiver model - -When a mailbox is created, a user is allowed to send mail from and receive mail for his own mailbox address. - - Mailbox me@example.org is created. example.org is a primary domain. - Note: a mailbox cannot be created in an alias domain. - - me@example.org is only known as me@example.org. - me@example.org is allowed to send as me@example.org. - -We can add an alias domain for example.org: - - Alias domain alias.com is added and assigned to primary domain example.org. - me@example.org is now known as me@example.org and me@alias.com. - me@example.org is now allowed to send as me@example.org and me@alias.com. - -We can add aliases for a mailbox to receive mail for and to send from this new address. - -It is important to know, that you are not able to receive mail for `my-alias@my-alias-domain.tld`. You would need to create this particular alias. - - me@example.org is assigned the alias alias@example.org - me@example.org is now known as alias@example.org, me@alias.com, alias@example.org - - me@example.org is NOT known as alias@alias.com. - -Administrators and domain administrators can edit mailboxes to allow specific users to send as other mailbox users ("delegate" them). - -You can choose between mailbox users or completely disable the sender check for domains. - -### SOGo "mail from" addresses - -Mailbox users can, obviously, select their own mailbox address, as well as all alias addresses and aliases that exist through alias domains. - -If you want to select another _existing_ mailbox user as your "mail from" address, this user has to delegate you access through SOGo (see SOGo documentation). Moreover a mailcow (domain) administrator -needs to grant you access as described above. diff --git a/docs/images/logo.svg b/docs/images/logo.svg deleted file mode 100644 index ea3b2796..00000000 --- a/docs/images/logo.svg +++ /dev/null @@ -1,179 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index c48e0d17..00000000 --- a/docs/index.md +++ /dev/null @@ -1,53 +0,0 @@ -# mailcow: dockerized - 🐮 + 🐋 = 💕 - -[![Servercow](https://www.servercow.de/img/cow_globe_200.svg)](https://www.servercow.de) - -If you want to support mailcow, consider hosting mailcow on a Servercow virtual machine @ Servercow! - -## Screenshots - -You can find screenshots [on Imgur](http://imgur.com/a/oewYt). - -## Overview - -mailcow dockerized comes with **12 containers** linked in **one bridged network**. -Each container represents a single application. - -- Dovecot -- ClamAV -- Memcached -- Redis -- MySQL -- Bind9 (Resolver) (formerly PDNS Recursor) -- PHP-FPM -- Postfix -- Nginx -- Rmilter -- Rspamd -- SOGo - -**7 volumes** to keep dynamic data - take care of them! - -- vmail-vol-1 -- dkim-vol-1 -- redis-vol-1 -- mysql-vol-1 -- rspamd-vol-1 -- postfix-vol-1 -- crypt-vol-1 - -The integrated **mailcow UI** allows administrative work on your mail server instance as well as separated domain administrator and mailbox user access: - -- DKIM key management -- Black- and whitelists per domain and per user -- Spam score managment per-user (reject spam, mark spam, greylist) -- Allow mailbox users to create temporary spam aliases -- Prepend mail tags to subject or move mail to subfolder (per-user) -- Allow mailbox users to toggle incoming and outgoing TLS enforcement -- Allow users to reset SOGo ActiveSync device caches -- imapsync to migrate or pull remote mailboxes regularly -- TFA: Yubi OTP and U2F USB (Google Chrome and derivates only) -- Add domains, mailboxes, aliases, domain aliases and SOGo resources -- Add whitelisted hosts to forward mail to mailcow - -*[Looking for a farm to host your cow?](https://www.servercow.de)* diff --git a/docs/install.md b/docs/install.md deleted file mode 100644 index b9322f00..00000000 --- a/docs/install.md +++ /dev/null @@ -1,126 +0,0 @@ -## Install mailcow - -**WARNING**: Please use Ubuntu 16.04 instead of Debian 8 or [switch to the kernel 4.9 from jessie backports](https://packages.debian.org/jessie-backports/linux-image-amd64) because there is a bug (kernel panic) with the kernel 3.16 when running docker containers with healthchecks! Full details here: [github.com/docker/docker/issues/30402](https://github.com/docker/docker/issues/30402) and [forum.mailcow.email/t/solved-mailcow-docker-causes-kernel-panic-edit/448](https://forum.mailcow.email/t/solved-mailcow-docker-causes-kernel-panic-edit/448) - -You need Docker and Docker Compose. - -1\. Learn how to install [Docker](https://docs.docker.com/engine/installation/linux/) and [Docker Compose](https://docs.docker.com/compose/install/). - -Quick installation for most operation systems: - -- Docker -``` -curl -sSL https://get.docker.com/ | sh -``` - -- Docker-Compose -``` -curl -L https://github.com/docker/compose/releases/download/$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose -chmod +x /usr/local/bin/docker-compose -``` - -Please use the latest Docker engine available and do not use the engine that ships with your distros repository. - -2\. Clone the master branch of the repository -``` -git clone https://github.com/andryyy/mailcow-dockerized && cd mailcow-dockerized -``` - -3\. Generate a configuration file. Use a FQDN (`host.domain.tld`) as hostname when asked. -``` -./generate_config.sh -``` - -4\. Change configuration if you want or need to. -``` -nano mailcow.conf -``` -If you plan to use a reverse proxy, you can, for example, bind HTTPS to 127.0.0.1 on port 8443 and HTTP to 127.0.0.1 on port 8080. - -You may need to stop an existing pre-installed MTA which blocks port 25/tcp. See [this chapter](https://andryyy.github.io/mailcow-dockerized/first_steps/#install-a-local-mta) to learn how to reconfigure Postfix to run besides mailcow after a successful installation. - -5\. Pull the images and run the composer file. The paramter `-d` will start mailcow: dockerized detached: -``` -docker-compose pull -docker-compose up -d -``` - -Done! - -You can now access **https://${MAILCOW_HOSTNAME}** with the default credentials `admin` + password `moohoo`. - -The database will be initialized right after a connection to MySQL can be established. - -## Update mailcow - -There is no update routine. You need to refresh your pulled repository clone and apply your local changes (if any). Actually there are many ways to merge local changes. - -### Step 1, method 1 -Stash all local changes, pull changes from the remote master branch and apply your stash on top of it. You will most likely see warnings about non-commited changes; you can ignore them: - -``` -# Stash local changes -git stash -# Re-pull master -git pull -# Apply stash and remove it -git stash pop -``` - -### Step 1, method 2 -Fetch new data from GitHub, commit changes and merge remote repository: - -``` -# Get updates/changes -git fetch -# Add all changed files to local clone -git add -A -# Commit changes, ignore git complaining about username and mail address -git commit -m "Local config aat $(date)" -# Merge changes -git merge -``` - -If git complains about conflicts, solve them! Example: -``` -CONFLICT (content): Merge conflict in data/web/index.php -``` - -Open `data/web/index.php`, solve the conflict, close the file and run `git add -A` + `git commit -m "Solved conflict"`. - -### Step 1, method 3 - -Thanks to fabreg @ GitHub! - -In case both methods do not work (for many reason like you're unable to fix the CONFLICTS or any other reasons) you can simply start all over again. - -Keep in mind that all local changes _to configuration files_ will be lost. However, your volumes will not be removed. - -- Copy mailcow.conf somewhere outside the mailcow-dockerized directory -- Stop and remove mailcow containers: `docker-compose down` -- Delete the directory or rename it -- Clone the remote repository again (`git clone https://github.com/andryyy/mailcow-dockerized && cd mailcow-dockerized`). **Pay attention** to this step - the folder must have the same name of the previous one! -- Copy back your previous `mailcow.conf` into the mailcow-dockerizd folder - -If you forgot to stop Docker before deleting the cloned directoy, you can use the following commands: -``` -docker stop $(docker ps -a -q) -docker rm $(docker ps -a -q) -``` - -### Step 2 - -Pull new images (if any) and recreate changed containers: - -``` -docker-compose pull -docker-compose up -d --remove-orphans -``` - -### Step 3 -Clean-up dangling (unused) images and volumes: - -``` -docker rmi -f $(docker images -f "dangling=true" -q) -docker volume rm $(docker volume ls -qf dangling=true) -``` diff --git a/docs/u_and_e.md b/docs/u_and_e.md deleted file mode 100644 index d61b81eb..00000000 --- a/docs/u_and_e.md +++ /dev/null @@ -1,536 +0,0 @@ -## mailcow UI configuration - -Several configuration parameters of the mailcow UI can be changed by creating a file `data/web/inc/vars.local.inc.php` which overrides defaults settings found in `data/web/inc/vars.inc.php`. - -The local configuration file is persistent over updates of mailcow. Try not to change values inside `data/web/inc/vars.inc.php`, but use them as template for the local override. - -mailcow UI configuration parameters can be to... - -- ...change the default language* -- ...change the default bootstrap theme -- ...set a password complexity regex -- ...add mailcow app buttons to the login screen -- ...set a pagination trigger -- ...set action after submitting forms (stay in form, return to previous page) - -\* To change SOGos default language, you will need to edit `data/conf/sogo/sogo.conf` and replace "English" by your preferred language. - -## Anonymize headers - -Save as `data/conf/postfix/mailcow_anonymize_headers.pcre`: - -``` -/^\s*Received:[^\)]+\)\s+\(Authenticated sender:(.+)/ - REPLACE Received: from localhost (localhost [127.0.0.1]) (Authenticated sender:$1 -/^\s*User-Agent/ IGNORE -/^\s*X-Enigmail/ IGNORE -/^\s*X-Mailer/ IGNORE -/^\s*X-Originating-IP/ IGNORE -/^\s*X-Forward/ IGNORE -``` - -Add this to `data/conf/postfix/main.cf`: -``` -smtp_header_checks = pcre:/opt/postfix/conf/mailcow_anonymize_headers.pcre -``` - -## Backup and restore maildir (simple tar file) - -### Backup - -This line backups the vmail directory to a file backup_vmail.tar.gz in the mailcow root directory: -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -DATE=$(date +"%Y%m%d_%H%M%S") -docker run --rm -it -v $(docker inspect --format '{{ range .Mounts }}{{ if eq .Destination "/var/vmail" }}{{ .Name }}{{ end }}{{ end }}' $(docker-compose ps -q dovecot-mailcow)):/vmail -v ${PWD}:/backup debian:jessie tar cvfz /backup/backup_vmail.tar.gz /vmail -``` - -You can change the path by adjusting ${PWD} (which equals to the current directory) to any path you have write-access to. -Set the filename `backup_vmail.tar.gz` to any custom name, but leave the path as it is. Example: `[...] tar cvfz /backup/my_own_filename_.tar.gz` - -### Restore -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -DATE=$(date +"%Y%m%d_%H%M%S") -docker run --rm -it -v $(docker inspect --format '{{ range .Mounts }}{{ if eq .Destination "/var/vmail" }}{{ .Name }}{{ end }}{{ end }}' $(docker-compose ps -q dovecot-mailcow)):/vmail -v ${PWD}:/backup debian:jessie tar xvfz /backup/backup_vmail.tar.gz -``` - -## Docker Compose Bash completion - -For the tab-tab... :-) - -``` -curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose version --short)/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose -``` -## Black and Whitelist - -Edit a domain as (domain) administrator to add an item to the filter table. - -Beware that a mailbox user can login to mailcow and override a domain policy filter item. - -## Customize Dockerfiles - -Make your changes in `data/Dockerfiles/$service` and build the image locally: - -``` -docker build data/Dockerfiles/service -t mailcow/$service -``` - -Now auto-recreate modified containers: - -``` -docker-compose up -d -``` - -## Disable sender addresses verification - -This option is not best-practice and should only be implemented when there is no other option available to archive whatever you are trying to do. - -Simply create a file `data/conf/postfix/check_sasl_access` and enter the following content. This user must exist in your installation and needs to authenticate before sending mail. -``` -user-to-allow-everything@example.com OK -``` - -Open `data/conf/postfix/main.cf` and find `smtpd_sender_restrictions`. Prepend `check_sasl_access hash:/opt/postfix/conf/check_sasl_access` like this: -``` -smtpd_sender_restrictions = check_sasl_access hash:/opt/postfix/conf/check_sasl_access reject_authenticated_sender_login_mismatch [...] -``` - -Run postmap on check_sasl_access: - -``` -docker-compose exec postfix-mailcow postmap /opt/postfix/conf/check_sasl_access -``` - -Restart the Postfix container. - -## Install Roundcube - -Download Roundcube 1.3.x (beta at the time of Feb 2017) to the web htdocs directory and extract it (here `rc/`): -``` -cd data/web/rc -wget -O - https://github.com/roundcube/roundcubemail/releases/download/1.3-beta/roundcubemail-1.3-beta-complete.tar.gz | tar xfvz - -# Change folder name -mv roundcubemail-1.3* rc -# Change permissions -chown -R root: rc/ -``` - -Create a file `data/web/rc/config/config.inc.php` with the following content. - -**Change the `des_key` parameter to a random value.** It is used to temporarily store your IMAP password. - -``` - array('verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true) -); -$config['enable_installer'] = false; -$config['smtp_conn_options'] = array( -'ssl' => array('verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true) -); -``` - -Point your browser to `https://myserver/rc/installer` and follow the instructions. -Initialize the database and leave the installer. - -**Delete the directory `data/web/rc/installer` after a successful installation!** - -### Enable change password function in Roundcube - -Open `data/web/rc/config/config.inc.php` and enable the password plugin: - -``` -... -$config['plugins'] = array( - 'archive', - 'password', -); -... -``` - -Open `data/web/rc/plugins/password/password.php`, search for `case 'ssha':` and add above: - -``` - case 'ssha256': - $salt = rcube_utils::random_bytes(8); - $crypted = base64_encode( hash('sha256', $password . $salt, TRUE ) . $salt ); - $prefix = '{SSHA256}'; - break; -``` - -Open `data/web/rc/plugins/password/config.inc.php` and change the following parameters (or add them at the bottom of that file): - -``` -$config['password_driver'] = 'sql'; -$config['password_algorithm'] = 'ssha256'; -$config['password_algorithm_prefix'] = '{SSHA256}'; -$config['password_query'] = "UPDATE mailbox SET password = %P WHERE username = %u"; -``` - -## MySQL - -### Connect -``` -source mailcow.conf -docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -``` - -### Backup -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -DATE=$(date +"%Y%m%d_%H%M%S") -docker-compose exec mysql-mailcow mysqldump --default-character-set=utf8mb4 -u${DBUSER} -p${DBPASS} ${DBNAME} > backup_${DBNAME}_${DATE}.sql -``` - -### Restore -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} < backup_file.sql -``` - -### Reset MySQL passwords - -Stop the stack by running `docker-compose stop`. - -When the containers came to a stop, run this command: - -``` -docker-compose run --rm --entrypoint '/bin/sh -c "gosu mysql mysqld --skip-grant-tables & sleep 10 && mysql -hlocalhost -uroot && exit 0"' mysql-mailcow -``` - -**1\. Find database name** - -``` -MariaDB [(none)]> show databases; -+--------------------+ -| Database | -+--------------------+ -| information_schema | -| mailcow_database | <===== -| mysql | -| performance_schema | -+--------------------+ -4 rows in set (0.00 sec) -``` - -**2\. Reset one or more users** - -Both "password" and "authentication_string" exist. Currently "password" is used, but better set both. - -``` -MariaDB [(none)]> SELECT user FROM mysql.user; -+--------------+ -| user | -+--------------+ -| mailcow_user | <===== -| root | -+--------------+ -2 rows in set (0.00 sec) - -MariaDB [(none)]> FLUSH PRIVILEGES; -MariaDB [(none)]> UPDATE mysql.user SET authentication_string = PASSWORD('gotr00t'), password = PASSWORD('gotr00t') WHERE User = 'root' AND Host = '%'; -MariaDB [(none)]> UPDATE mysql.user SET authentication_string = PASSWORD('mookuh'), password = PASSWORD('mookuh') WHERE User = 'mailcow' AND Host = '%'; -MariaDB [(none)]> FLUSH PRIVILEGES; -``` - -## Debugging - -You can use `docker-compose logs $service-name` for all containers. - -Run `docker-compose logs` for all logs at once. - -Follow the log output by running docker-compose with `logs -f`. - -Limit the output by calling logs with `--tail=300` like `docker-compose logs --tail=300 mysql-mailcow`. - -## Redirect port 80 to 443 - -Since February the 28th 2017 mailcow does come with port 80 and 443 enabled. - -Open `mailcow.conf` and set `HTTP_BIND=0.0.0.0`. - -Open `data/conf/nginx/site.conf` and add a new "catch-all" site at the top of that file: - -``` -server { - listen 80 default_server; - include /etc/nginx/conf.d/server_name.active; - return 301 https://$host$request_uri; -} -``` - -Restart the stack, changed containers will be updated: - -`docker-compose up -d` - -## Redis - -### Client - -``` -docker-compose exec redis-mailcow redis-cli -``` - -## Remove persistent data - -- Remove volume `mysql-vol-1` to remove all MySQL data. -- Remove volume `redis-vol-1` to remove all Redis data. -- Remove volume `vmail-vol-1` to remove all contents of `/var/vmail` mounted to `dovecot-mailcow`. -- Remove volume `dkim-vol-1` to remove all DKIM keys. -- Remove volume `rspamd-vol-1` to remove all Rspamd data. - -Running `docker-compose down -v` will **destroy all mailcow: dockerized volumes** and delete any related containers. - -## Reset admin password -Reset mailcow admin to `admin:moohoo`: - -``` -cd mailcow_path -bash reset_admin.sh -``` - -## Rspamd - -### Learn spam and ham - -Rspamd learns mail as spam or ham when you move a message in or out of the junk folder to any mailbox besides trash. -This is archived by using the Dovecot plugin "antispam" and a simple parser script. - -Rspamd also auto-learns mail when a high or low score is detected (see https://rspamd.com/doc/configuration/statistic.html#autolearning) - -The bayes statistics are written to Redis as keys `BAYES_HAM` and `BAYES_SPAM`. - -You can also use Rspamd's web ui to learn ham and/or spam. - -### Learn ham or spam from existing directory - -You can use a one-liner to learn mail in plain-text (uncompressed) format: -``` -# Ham -for file in /my/folder/cur/*; do docker exec -i $(docker-compose ps -q rspamd-mailcow) rspamc learn_ham < $file; done -# Spam -for file in /my/folder/.Junk/cur/*; do docker exec -i $(docker-compose ps -q rspamd-mailcow) rspamc learn_spam < $file; done -``` - -Consider attaching a local folder as new volume to `rspamd-mailcow` in `docker-compose.yml` and learn given files inside the container. This can be used as workaround to parse compressed data with zcat. Example: - -``` -for file in /data/old_mail/.Junk/cur/*; do rspamc learn_spam < zcat $file; done -``` - -### CLI tools - -``` -docker-compose exec rspamd-mailcow rspamc --help -docker-compose exec rspamd-mailcow rspamadm --help -``` - -See [Rspamd documentation](https://rspamd.com/doc/index.html) - -## Adjust service configurations - -The most important configuration files are mounted from the host into the related containers: - -``` -data/conf -├── bind9 -│   └── named.conf -├── dovecot -│   ├── dovecot.conf -│   ├── dovecot-master.passwd -│   ├── sieve_after -│   └── sql -│   ├── dovecot-dict-sql.conf -│   └── dovecot-mysql.conf -├── mysql -│   └── my.cnf -├── nginx -│   ├── dynmaps.conf -│   ├── site.conf -│   └── templates -│   ├── listen_plain.template -│   ├── listen_ssl.template -│   └── server_name.template -├── pdns -│   ├── pdns_custom.lua -│   └── recursor.conf -├── postfix -│   ├── main.cf -│   ├── master.cf -│   ├── postscreen_access.cidr -│   ├── smtp_dsn_filter -│   └── sql -│   ├── mysql_relay_recipient_maps.cf -│   ├── mysql_tls_enforce_in_policy.cf -│   ├── mysql_tls_enforce_out_policy.cf -│   ├── mysql_virtual_alias_domain_catchall_maps.cf -│   ├── mysql_virtual_alias_domain_maps.cf -│   ├── mysql_virtual_alias_maps.cf -│   ├── mysql_virtual_domains_maps.cf -│   ├── mysql_virtual_mailbox_maps.cf -│   ├── mysql_virtual_relay_domain_maps.cf -│   ├── mysql_virtual_sender_acl.cf -│   └── mysql_virtual_spamalias_maps.cf -├── rmilter -│   └── rmilter.conf -├── rspamd -│   ├── dynmaps -│   │   ├── authoritative.php -│   │   ├── settings.php -│   │   ├── tags.php -│   │   └── vars.inc.php -> ../../../web/inc/vars.inc.php -│   ├── local.d -│   │   ├── dkim.conf -│   │   ├── metrics.conf -│   │   ├── options.inc -│   │   ├── redis.conf -│   │   ├── rspamd.conf.local -│   │   └── statistic.conf -│   ├── lua -│   │   └── rspamd.local.lua -│   └── override.d -│   ├── logging.inc -│   ├── worker-controller.inc -│   └── worker-normal.inc -└── sogo - ├── sieve.creds - └── sogo.conf - -``` - -Just change the according configuration file on the host and restart the related service: -``` -docker-compose restart service-mailcow -``` - -## Tagging - -Mailbox users can tag their mail address like in `me+facebook@example.org` and choose between to setups to handle this tag: - -1\. Move this message to a subfolder "facebook" (will be created lower case if not existing) - -2\. Prepend the tag to the subject: "[facebook] Subject" - -## Two-factor authentication - -So far two methods for TFA are implemented. Both work with the fantastic [Yubikey](https://www.yubico.com). - -While Yubi OTP needs an active internet connection and an API ID and key, U2F will work with any FIDO U2F USB key out of the box, but can only be used when mailcow is accessed over HTTPS. - -Both methods support multiple YubiKeys. - -As administrator you are able to temporary disable a domain administrators TFA login until they successfully logged in. - -The key used to login will be displayed in green, while other keys remain grey. - -### Yubi OTP - -The Yubi API ID and Key will be checked against the Yubico Cloud API. When setting up TFA you will be asked for your personal API account for this key. -The API ID, API key and the first 12 characters (your YubiKeys ID in modhex) are stored in the MySQL table as secret. - -### U2F - -Only Google Chrome (+derivates) and Opera support U2F authentication to this day natively. -For Firefox you will need to install the "U2F Support Add-on" as provided on [mozilla.org](https://addons.mozilla.org/en-US/firefox/addon/u2f-support-add-on/). - -U2F works without an internet connection. - -## Portainer - -In order to enable Portainer, the docker-compose.yml and site.conf for nginx must be modified. - -1\. docker-compose.yml: Insert this block for portainer -``` - portainer-mailcow: - image: portainer/portainer - volumes: - - /var/run/docker.sock:/var/run/docker.sock - restart: always - dns: - - 172.22.1.254 - dns_search: mailcow-network - networks: - mailcow-network: - aliases: - - portainer -``` -2a\. data/conf/nginx/site.conf: Just beneath the opening line, at the same level as a server { block, add this: -``` -upstream portainer { - server portainer-mailcow:9000; -} - -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} -``` - -2b\. data/conf/nginx/site.conf: Then, inside **both** (ssl and plain) server blocks, add this: -``` - location /portainer/ { - proxy_http_version 1.1; - proxy_set_header Host $http_host; # required for docker client's sake - proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 900; - - proxy_set_header Connection ""; - proxy_buffers 32 4k; - proxy_pass http://portainer/; - } - - location /portainer/api/websocket/ { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_pass http://portainer/api/websocket/; - } -``` - -Now you can simply navigate to https://${MAILCOW_HOSTNAME}/portainer/ to view your Portainer container monitoring page. You’ll then be prompted to specify a new password for the **admin** account. After specifying your password, you’ll then be able to connect to the Portainer UI. - -## Change autodiscover setup type - -This disables ActiveSync in the autodiscover service for Outlook and configures it with IMAP and SMTP instead: - -Open `data/web/autodiscover.php` and set `'useEASforOutlook' => 'yes'` to `'useEASforOutlook' => 'no'`. - -To always use IMAP and SMTP instead of EAS, set `'autodiscoverType' => 'imap'`. - -## Why Bind? - -For DNS blacklist lookups and DNSSEC. - -Most systems use either a public or a local caching DNS resolver. -That's a very bad idea when it comes to filter spam using DNS-based blackhole lists (DNSBL) or similar technics. -Most if not all providers apply a rate limit based on the DNS resolver that is used to query their service. -Using a public resolver like Googles 4x8, OpenDNS or any other shared DNS resolver like your ISPs will hit that limit very soon. From 2f4e9305eb7d0464ebc0e8c890b9a3262a2a8861 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 1 May 2017 21:25:19 +0200 Subject: [PATCH 095/108] Remove docs --- docs/first_steps.md | 301 ------------------------ docs/images/logo.svg | 179 --------------- docs/index.md | 53 ----- docs/install.md | 126 ---------- docs/u_and_e.md | 536 ------------------------------------------- 5 files changed, 1195 deletions(-) delete mode 100644 docs/first_steps.md delete mode 100644 docs/images/logo.svg delete mode 100644 docs/index.md delete mode 100644 docs/install.md delete mode 100644 docs/u_and_e.md diff --git a/docs/first_steps.md b/docs/first_steps.md deleted file mode 100644 index cba908e5..00000000 --- a/docs/first_steps.md +++ /dev/null @@ -1,301 +0,0 @@ -## SSL (and: How to use Let's Encrypt) - -mailcow dockerized comes with a snakeoil CA "mailcow" and a server certificate in `data/assets/ssl`. Please use your own trusted certificates. - -mailcow uses 3 domain names that should be covered by your new certificate: - -- ${MAILCOW_HOSTNAME} -- autodiscover.**example.org** -- autoconfig.**example.org** - -### Obtain multi-SAN certificate by Let's Encrypt - -This is just an example of how to obtain certificates with certbot. There are several methods! - -1\. Get the certbot client: -``` bash -wget https://dl.eff.org/certbot-auto -O /usr/local/sbin/certbot && chmod +x /usr/local/sbin/certbot -``` - -2\. Make sure you set `HTTP_BIND=0.0.0.0` and `HTTP_PORT=80` in `mailcow.conf` or setup a reverse proxy to enable connections to port 80. If you changed HTTP_BIND, then rebuild Nginx: -``` bash -docker-compose up -d -``` - -3\. Request the certificate with the webroot method: -``` bash -cd /path/to/git/clone/mailcow-dockerized -source mailcow.conf -certbot certonly \ - --webroot \ - -w ${PWD}/data/web \ - -d ${MAILCOW_HOSTNAME} \ - -d autodiscover.example.org \ - -d autoconfig.example.org \ - --email you@example.org \ - --agree-tos -``` - -**Remember to replace the example.org domain with your own domain, this command will not work if you dont.** - -4\. Create hard links to the full path of the new certificates. Assuming you are still in the mailcow root folder: -``` bash -mv data/assets/ssl/cert.{pem,pem.backup} -mv data/assets/ssl/key.{pem,pem.backup} -ln $(readlink -f /etc/letsencrypt/live/${MAILCOW_HOSTNAME}/fullchain.pem) data/assets/ssl/cert.pem -ln $(readlink -f /etc/letsencrypt/live/${MAILCOW_HOSTNAME}/privkey.pem) data/assets/ssl/key.pem -``` - -5\. Restart affected containers: -``` -docker-compose restart postfix-mailcow dovecot-mailcow nginx-mailcow -``` - -When renewing certificates, run the last two steps (link + restart) as post-hook in a script. - -## Rspamd Web UI -At first you may want to setup Rspamds web interface which provides some useful features and information. - -1\. Generate a Rspamd controller password hash: -``` -docker-compose exec rspamd-mailcow rspamadm pw -``` - -2\. Replace the default hash in `data/conf/rspamd/override.d/worker-controller.inc` by your newly generated: -``` -enable_password = "myhash"; -``` - -You can use `password = "myhash";` instead of `enable_password` to disable write-access in the web UI. - -3\. Restart rspamd: -``` -docker-compose restart rspamd-mailcow -``` - -Open https://${MAILCOW_HOSTNAME}/rspamd in a browser and login! - -## Optional: Reverse proxy - -You don't need to change the Nginx site that comes with mailcow: dockerized. -mailcow: dockerized trusts the default gateway IP 172.22.1.1 as proxy. This is very important to control access to Rspamd's web UI. - -1\. Make sure you change HTTP_BIND and HTTPS_BIND in `mailcow.conf` to a local address and set the ports accordingly, for example: -``` bash -HTTP_BIND=127.0.0.1 -HTTP_PORT=8080 -HTTPS_PORT=127.0.0.1 -HTTPS_PORT=8443 -``` -** IMPORTANT: Do not use port 8081 ** - -Recreate affected containers by running `docker-compose up -d`. - -2\. Configure your local webserver as reverse proxy: - -### Apache 2.4 -``` apache - - ServerName mail.example.org - ServerAlias autodiscover.example.org - ServerAlias autoconfig.example.org - - [...] - # You should proxy to a plain HTTP session to offload SSL processing - ProxyPass / http://127.0.0.1:8080/ - ProxyPassReverse / http://127.0.0.1:8080/ - ProxyPreserveHost Off - your-ssl-configuration-here - [...] - - # If you plan to proxy to a HTTPS host: - #SSLProxyEngine On - - # If you plan to proxy to an untrusted HTTPS host: - #SSLProxyVerify none - #SSLProxyCheckPeerCN off - #SSLProxyCheckPeerName off - #SSLProxyCheckPeerExpire off - -``` - -### Nginx -``` -server { - listen 443; - server_name mail.example.org autodiscover.example.org autoconfig.example.org; - - [...] - your-ssl-configuration-here - location / { - proxy_pass http://127.0.0.1:8080/; - proxy_redirect http://127.0.0.1:8080/ $scheme://$host:$server_port/; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - [...] -} -``` - -## Optional: Setup a relayhost - -Insert these lines to `data/conf/postfix/main.cf`. "relayhost" does already exist (empty), just change its value. -``` -relayhost = [your-relayhost]:587 -smtp_sasl_password_maps = hash:/opt/postfix/conf/smarthost_passwd -smtp_sasl_auth_enable = yes -``` - -Create the credentials file: -``` -echo "your-relayhost username:password" > data/conf/postfix/smarthost_passwd -``` - -Run: -``` -docker-compose exec postfix-mailcow postmap /opt/postfix/conf/smarthost_passwd -docker-compose exec postfix-mailcow chown root:postfix /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db -docker-compose exec postfix-mailcow chmod 660 /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db -docker-compose exec postfix-mailcow postfix reload -``` - -### Helper script - -There is a helper script `mailcow-setup-relayhost.sh` you can run to setup a relayhost. - -``` bash -Usage: - -Setup a relayhost: -./mailcow-setup-relayhost.sh relayhost port (username) (password) -Username and password are optional parameters. - -Reset to defaults: -./mailcow-setup-relayhost.sh reset -``` - -## Optional: Log to Syslog - -Enable Rsyslog to receive logs on 524/tcp: - -``` -# This setting depends on your Rsyslog version and configuration format. -# For most Debian derivates it will work like this... -$ModLoad imtcp -$TCPServerAddress 127.0.0.1 -$InputTCPServerRun 524 - -# ...while for Ubuntu 16.04 it looks like this: -module(load="imtcp") -input(type="imtcp" address="127.0.0.1" port="524") - -# No matter your Rsyslog version, you should set this option to off -# if you plan to use Fail2ban -$RepeatedMsgReduction off -``` - -Restart rsyslog after enabling the TCP listener. - -Now setup Docker daemon to start with the syslog driver. -This enables the syslog driver for all containers! - -Debian users can change the startup configuration in `/etc/default/docker` while CentOS users find it in `/etc/sysconfig/docker`: -``` -... -DOCKER_OPTS="--log-driver=syslog --log-opt syslog-address=tcp://127.0.0.1:524" -... -``` - -**Caution:** For some reason Ubuntu 16.04 and some, but not all, systemd based distros do not read the defaults file parameters. - -Just run `systemctl edit docker.service` and add the following content to fix it. - -**Note:** If "systemctl edit" is not available, just copy the content to `/etc/systemd/system/docker.service.d/override.conf`. - -The first empty ExecStart parameter is not a mistake. - -``` -[Service] -EnvironmentFile=/etc/default/docker -ExecStart= -ExecStart=/usr/bin/docker daemon -H fd:// $DOCKER_OPTS -``` - -Restart the Docker daemon and run `docker-compose down && docker-compose up -d` to recreate the containers. - -### Use Fail2ban - -**This is a subsection of "Log to Syslog", which is required for Fail2ban to work.** - -Open `/etc/fail2ban/filter.d/common.conf` and search for the prefix_line parameter, change it to ".*": - -``` -__prefix_line = .* -``` - -Create `/etc/fail2ban/jail.d/dovecot.conf`... -``` -[dovecot] -enabled = true -filter = dovecot -logpath = /var/log/syslog -chain = FORWARD -``` - -and `jail.d/postfix-sasl.conf`: -``` -[postfix-sasl] -enabled = true -filter = postfix-sasl -logpath = /var/log/syslog -chain = FORWARD -``` - -Restart Fail2ban. - -## Install a local MTA - -The easiest option would be to disable the listener on port 25/tcp. - -**Postfix** users disable the listener by commenting the following line (starting with `smtp` or `25`) in `/etc/postfix/master.cf`: -``` -#smtp inet n - - - - smtpd -``` -Restart Postfix after applying your changes. - -## Sender and receiver model - -When a mailbox is created, a user is allowed to send mail from and receive mail for his own mailbox address. - - Mailbox me@example.org is created. example.org is a primary domain. - Note: a mailbox cannot be created in an alias domain. - - me@example.org is only known as me@example.org. - me@example.org is allowed to send as me@example.org. - -We can add an alias domain for example.org: - - Alias domain alias.com is added and assigned to primary domain example.org. - me@example.org is now known as me@example.org and me@alias.com. - me@example.org is now allowed to send as me@example.org and me@alias.com. - -We can add aliases for a mailbox to receive mail for and to send from this new address. - -It is important to know, that you are not able to receive mail for `my-alias@my-alias-domain.tld`. You would need to create this particular alias. - - me@example.org is assigned the alias alias@example.org - me@example.org is now known as alias@example.org, me@alias.com, alias@example.org - - me@example.org is NOT known as alias@alias.com. - -Administrators and domain administrators can edit mailboxes to allow specific users to send as other mailbox users ("delegate" them). - -You can choose between mailbox users or completely disable the sender check for domains. - -### SOGo "mail from" addresses - -Mailbox users can, obviously, select their own mailbox address, as well as all alias addresses and aliases that exist through alias domains. - -If you want to select another _existing_ mailbox user as your "mail from" address, this user has to delegate you access through SOGo (see SOGo documentation). Moreover a mailcow (domain) administrator -needs to grant you access as described above. diff --git a/docs/images/logo.svg b/docs/images/logo.svg deleted file mode 100644 index ea3b2796..00000000 --- a/docs/images/logo.svg +++ /dev/null @@ -1,179 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index c48e0d17..00000000 --- a/docs/index.md +++ /dev/null @@ -1,53 +0,0 @@ -# mailcow: dockerized - 🐮 + 🐋 = 💕 - -[![Servercow](https://www.servercow.de/img/cow_globe_200.svg)](https://www.servercow.de) - -If you want to support mailcow, consider hosting mailcow on a Servercow virtual machine @ Servercow! - -## Screenshots - -You can find screenshots [on Imgur](http://imgur.com/a/oewYt). - -## Overview - -mailcow dockerized comes with **12 containers** linked in **one bridged network**. -Each container represents a single application. - -- Dovecot -- ClamAV -- Memcached -- Redis -- MySQL -- Bind9 (Resolver) (formerly PDNS Recursor) -- PHP-FPM -- Postfix -- Nginx -- Rmilter -- Rspamd -- SOGo - -**7 volumes** to keep dynamic data - take care of them! - -- vmail-vol-1 -- dkim-vol-1 -- redis-vol-1 -- mysql-vol-1 -- rspamd-vol-1 -- postfix-vol-1 -- crypt-vol-1 - -The integrated **mailcow UI** allows administrative work on your mail server instance as well as separated domain administrator and mailbox user access: - -- DKIM key management -- Black- and whitelists per domain and per user -- Spam score managment per-user (reject spam, mark spam, greylist) -- Allow mailbox users to create temporary spam aliases -- Prepend mail tags to subject or move mail to subfolder (per-user) -- Allow mailbox users to toggle incoming and outgoing TLS enforcement -- Allow users to reset SOGo ActiveSync device caches -- imapsync to migrate or pull remote mailboxes regularly -- TFA: Yubi OTP and U2F USB (Google Chrome and derivates only) -- Add domains, mailboxes, aliases, domain aliases and SOGo resources -- Add whitelisted hosts to forward mail to mailcow - -*[Looking for a farm to host your cow?](https://www.servercow.de)* diff --git a/docs/install.md b/docs/install.md deleted file mode 100644 index b9322f00..00000000 --- a/docs/install.md +++ /dev/null @@ -1,126 +0,0 @@ -## Install mailcow - -**WARNING**: Please use Ubuntu 16.04 instead of Debian 8 or [switch to the kernel 4.9 from jessie backports](https://packages.debian.org/jessie-backports/linux-image-amd64) because there is a bug (kernel panic) with the kernel 3.16 when running docker containers with healthchecks! Full details here: [github.com/docker/docker/issues/30402](https://github.com/docker/docker/issues/30402) and [forum.mailcow.email/t/solved-mailcow-docker-causes-kernel-panic-edit/448](https://forum.mailcow.email/t/solved-mailcow-docker-causes-kernel-panic-edit/448) - -You need Docker and Docker Compose. - -1\. Learn how to install [Docker](https://docs.docker.com/engine/installation/linux/) and [Docker Compose](https://docs.docker.com/compose/install/). - -Quick installation for most operation systems: - -- Docker -``` -curl -sSL https://get.docker.com/ | sh -``` - -- Docker-Compose -``` -curl -L https://github.com/docker/compose/releases/download/$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose -chmod +x /usr/local/bin/docker-compose -``` - -Please use the latest Docker engine available and do not use the engine that ships with your distros repository. - -2\. Clone the master branch of the repository -``` -git clone https://github.com/andryyy/mailcow-dockerized && cd mailcow-dockerized -``` - -3\. Generate a configuration file. Use a FQDN (`host.domain.tld`) as hostname when asked. -``` -./generate_config.sh -``` - -4\. Change configuration if you want or need to. -``` -nano mailcow.conf -``` -If you plan to use a reverse proxy, you can, for example, bind HTTPS to 127.0.0.1 on port 8443 and HTTP to 127.0.0.1 on port 8080. - -You may need to stop an existing pre-installed MTA which blocks port 25/tcp. See [this chapter](https://andryyy.github.io/mailcow-dockerized/first_steps/#install-a-local-mta) to learn how to reconfigure Postfix to run besides mailcow after a successful installation. - -5\. Pull the images and run the composer file. The paramter `-d` will start mailcow: dockerized detached: -``` -docker-compose pull -docker-compose up -d -``` - -Done! - -You can now access **https://${MAILCOW_HOSTNAME}** with the default credentials `admin` + password `moohoo`. - -The database will be initialized right after a connection to MySQL can be established. - -## Update mailcow - -There is no update routine. You need to refresh your pulled repository clone and apply your local changes (if any). Actually there are many ways to merge local changes. - -### Step 1, method 1 -Stash all local changes, pull changes from the remote master branch and apply your stash on top of it. You will most likely see warnings about non-commited changes; you can ignore them: - -``` -# Stash local changes -git stash -# Re-pull master -git pull -# Apply stash and remove it -git stash pop -``` - -### Step 1, method 2 -Fetch new data from GitHub, commit changes and merge remote repository: - -``` -# Get updates/changes -git fetch -# Add all changed files to local clone -git add -A -# Commit changes, ignore git complaining about username and mail address -git commit -m "Local config aat $(date)" -# Merge changes -git merge -``` - -If git complains about conflicts, solve them! Example: -``` -CONFLICT (content): Merge conflict in data/web/index.php -``` - -Open `data/web/index.php`, solve the conflict, close the file and run `git add -A` + `git commit -m "Solved conflict"`. - -### Step 1, method 3 - -Thanks to fabreg @ GitHub! - -In case both methods do not work (for many reason like you're unable to fix the CONFLICTS or any other reasons) you can simply start all over again. - -Keep in mind that all local changes _to configuration files_ will be lost. However, your volumes will not be removed. - -- Copy mailcow.conf somewhere outside the mailcow-dockerized directory -- Stop and remove mailcow containers: `docker-compose down` -- Delete the directory or rename it -- Clone the remote repository again (`git clone https://github.com/andryyy/mailcow-dockerized && cd mailcow-dockerized`). **Pay attention** to this step - the folder must have the same name of the previous one! -- Copy back your previous `mailcow.conf` into the mailcow-dockerizd folder - -If you forgot to stop Docker before deleting the cloned directoy, you can use the following commands: -``` -docker stop $(docker ps -a -q) -docker rm $(docker ps -a -q) -``` - -### Step 2 - -Pull new images (if any) and recreate changed containers: - -``` -docker-compose pull -docker-compose up -d --remove-orphans -``` - -### Step 3 -Clean-up dangling (unused) images and volumes: - -``` -docker rmi -f $(docker images -f "dangling=true" -q) -docker volume rm $(docker volume ls -qf dangling=true) -``` diff --git a/docs/u_and_e.md b/docs/u_and_e.md deleted file mode 100644 index d61b81eb..00000000 --- a/docs/u_and_e.md +++ /dev/null @@ -1,536 +0,0 @@ -## mailcow UI configuration - -Several configuration parameters of the mailcow UI can be changed by creating a file `data/web/inc/vars.local.inc.php` which overrides defaults settings found in `data/web/inc/vars.inc.php`. - -The local configuration file is persistent over updates of mailcow. Try not to change values inside `data/web/inc/vars.inc.php`, but use them as template for the local override. - -mailcow UI configuration parameters can be to... - -- ...change the default language* -- ...change the default bootstrap theme -- ...set a password complexity regex -- ...add mailcow app buttons to the login screen -- ...set a pagination trigger -- ...set action after submitting forms (stay in form, return to previous page) - -\* To change SOGos default language, you will need to edit `data/conf/sogo/sogo.conf` and replace "English" by your preferred language. - -## Anonymize headers - -Save as `data/conf/postfix/mailcow_anonymize_headers.pcre`: - -``` -/^\s*Received:[^\)]+\)\s+\(Authenticated sender:(.+)/ - REPLACE Received: from localhost (localhost [127.0.0.1]) (Authenticated sender:$1 -/^\s*User-Agent/ IGNORE -/^\s*X-Enigmail/ IGNORE -/^\s*X-Mailer/ IGNORE -/^\s*X-Originating-IP/ IGNORE -/^\s*X-Forward/ IGNORE -``` - -Add this to `data/conf/postfix/main.cf`: -``` -smtp_header_checks = pcre:/opt/postfix/conf/mailcow_anonymize_headers.pcre -``` - -## Backup and restore maildir (simple tar file) - -### Backup - -This line backups the vmail directory to a file backup_vmail.tar.gz in the mailcow root directory: -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -DATE=$(date +"%Y%m%d_%H%M%S") -docker run --rm -it -v $(docker inspect --format '{{ range .Mounts }}{{ if eq .Destination "/var/vmail" }}{{ .Name }}{{ end }}{{ end }}' $(docker-compose ps -q dovecot-mailcow)):/vmail -v ${PWD}:/backup debian:jessie tar cvfz /backup/backup_vmail.tar.gz /vmail -``` - -You can change the path by adjusting ${PWD} (which equals to the current directory) to any path you have write-access to. -Set the filename `backup_vmail.tar.gz` to any custom name, but leave the path as it is. Example: `[...] tar cvfz /backup/my_own_filename_.tar.gz` - -### Restore -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -DATE=$(date +"%Y%m%d_%H%M%S") -docker run --rm -it -v $(docker inspect --format '{{ range .Mounts }}{{ if eq .Destination "/var/vmail" }}{{ .Name }}{{ end }}{{ end }}' $(docker-compose ps -q dovecot-mailcow)):/vmail -v ${PWD}:/backup debian:jessie tar xvfz /backup/backup_vmail.tar.gz -``` - -## Docker Compose Bash completion - -For the tab-tab... :-) - -``` -curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose version --short)/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose -``` -## Black and Whitelist - -Edit a domain as (domain) administrator to add an item to the filter table. - -Beware that a mailbox user can login to mailcow and override a domain policy filter item. - -## Customize Dockerfiles - -Make your changes in `data/Dockerfiles/$service` and build the image locally: - -``` -docker build data/Dockerfiles/service -t mailcow/$service -``` - -Now auto-recreate modified containers: - -``` -docker-compose up -d -``` - -## Disable sender addresses verification - -This option is not best-practice and should only be implemented when there is no other option available to archive whatever you are trying to do. - -Simply create a file `data/conf/postfix/check_sasl_access` and enter the following content. This user must exist in your installation and needs to authenticate before sending mail. -``` -user-to-allow-everything@example.com OK -``` - -Open `data/conf/postfix/main.cf` and find `smtpd_sender_restrictions`. Prepend `check_sasl_access hash:/opt/postfix/conf/check_sasl_access` like this: -``` -smtpd_sender_restrictions = check_sasl_access hash:/opt/postfix/conf/check_sasl_access reject_authenticated_sender_login_mismatch [...] -``` - -Run postmap on check_sasl_access: - -``` -docker-compose exec postfix-mailcow postmap /opt/postfix/conf/check_sasl_access -``` - -Restart the Postfix container. - -## Install Roundcube - -Download Roundcube 1.3.x (beta at the time of Feb 2017) to the web htdocs directory and extract it (here `rc/`): -``` -cd data/web/rc -wget -O - https://github.com/roundcube/roundcubemail/releases/download/1.3-beta/roundcubemail-1.3-beta-complete.tar.gz | tar xfvz - -# Change folder name -mv roundcubemail-1.3* rc -# Change permissions -chown -R root: rc/ -``` - -Create a file `data/web/rc/config/config.inc.php` with the following content. - -**Change the `des_key` parameter to a random value.** It is used to temporarily store your IMAP password. - -``` - array('verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true) -); -$config['enable_installer'] = false; -$config['smtp_conn_options'] = array( -'ssl' => array('verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true) -); -``` - -Point your browser to `https://myserver/rc/installer` and follow the instructions. -Initialize the database and leave the installer. - -**Delete the directory `data/web/rc/installer` after a successful installation!** - -### Enable change password function in Roundcube - -Open `data/web/rc/config/config.inc.php` and enable the password plugin: - -``` -... -$config['plugins'] = array( - 'archive', - 'password', -); -... -``` - -Open `data/web/rc/plugins/password/password.php`, search for `case 'ssha':` and add above: - -``` - case 'ssha256': - $salt = rcube_utils::random_bytes(8); - $crypted = base64_encode( hash('sha256', $password . $salt, TRUE ) . $salt ); - $prefix = '{SSHA256}'; - break; -``` - -Open `data/web/rc/plugins/password/config.inc.php` and change the following parameters (or add them at the bottom of that file): - -``` -$config['password_driver'] = 'sql'; -$config['password_algorithm'] = 'ssha256'; -$config['password_algorithm_prefix'] = '{SSHA256}'; -$config['password_query'] = "UPDATE mailbox SET password = %P WHERE username = %u"; -``` - -## MySQL - -### Connect -``` -source mailcow.conf -docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -``` - -### Backup -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -DATE=$(date +"%Y%m%d_%H%M%S") -docker-compose exec mysql-mailcow mysqldump --default-character-set=utf8mb4 -u${DBUSER} -p${DBPASS} ${DBNAME} > backup_${DBNAME}_${DATE}.sql -``` - -### Restore -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} < backup_file.sql -``` - -### Reset MySQL passwords - -Stop the stack by running `docker-compose stop`. - -When the containers came to a stop, run this command: - -``` -docker-compose run --rm --entrypoint '/bin/sh -c "gosu mysql mysqld --skip-grant-tables & sleep 10 && mysql -hlocalhost -uroot && exit 0"' mysql-mailcow -``` - -**1\. Find database name** - -``` -MariaDB [(none)]> show databases; -+--------------------+ -| Database | -+--------------------+ -| information_schema | -| mailcow_database | <===== -| mysql | -| performance_schema | -+--------------------+ -4 rows in set (0.00 sec) -``` - -**2\. Reset one or more users** - -Both "password" and "authentication_string" exist. Currently "password" is used, but better set both. - -``` -MariaDB [(none)]> SELECT user FROM mysql.user; -+--------------+ -| user | -+--------------+ -| mailcow_user | <===== -| root | -+--------------+ -2 rows in set (0.00 sec) - -MariaDB [(none)]> FLUSH PRIVILEGES; -MariaDB [(none)]> UPDATE mysql.user SET authentication_string = PASSWORD('gotr00t'), password = PASSWORD('gotr00t') WHERE User = 'root' AND Host = '%'; -MariaDB [(none)]> UPDATE mysql.user SET authentication_string = PASSWORD('mookuh'), password = PASSWORD('mookuh') WHERE User = 'mailcow' AND Host = '%'; -MariaDB [(none)]> FLUSH PRIVILEGES; -``` - -## Debugging - -You can use `docker-compose logs $service-name` for all containers. - -Run `docker-compose logs` for all logs at once. - -Follow the log output by running docker-compose with `logs -f`. - -Limit the output by calling logs with `--tail=300` like `docker-compose logs --tail=300 mysql-mailcow`. - -## Redirect port 80 to 443 - -Since February the 28th 2017 mailcow does come with port 80 and 443 enabled. - -Open `mailcow.conf` and set `HTTP_BIND=0.0.0.0`. - -Open `data/conf/nginx/site.conf` and add a new "catch-all" site at the top of that file: - -``` -server { - listen 80 default_server; - include /etc/nginx/conf.d/server_name.active; - return 301 https://$host$request_uri; -} -``` - -Restart the stack, changed containers will be updated: - -`docker-compose up -d` - -## Redis - -### Client - -``` -docker-compose exec redis-mailcow redis-cli -``` - -## Remove persistent data - -- Remove volume `mysql-vol-1` to remove all MySQL data. -- Remove volume `redis-vol-1` to remove all Redis data. -- Remove volume `vmail-vol-1` to remove all contents of `/var/vmail` mounted to `dovecot-mailcow`. -- Remove volume `dkim-vol-1` to remove all DKIM keys. -- Remove volume `rspamd-vol-1` to remove all Rspamd data. - -Running `docker-compose down -v` will **destroy all mailcow: dockerized volumes** and delete any related containers. - -## Reset admin password -Reset mailcow admin to `admin:moohoo`: - -``` -cd mailcow_path -bash reset_admin.sh -``` - -## Rspamd - -### Learn spam and ham - -Rspamd learns mail as spam or ham when you move a message in or out of the junk folder to any mailbox besides trash. -This is archived by using the Dovecot plugin "antispam" and a simple parser script. - -Rspamd also auto-learns mail when a high or low score is detected (see https://rspamd.com/doc/configuration/statistic.html#autolearning) - -The bayes statistics are written to Redis as keys `BAYES_HAM` and `BAYES_SPAM`. - -You can also use Rspamd's web ui to learn ham and/or spam. - -### Learn ham or spam from existing directory - -You can use a one-liner to learn mail in plain-text (uncompressed) format: -``` -# Ham -for file in /my/folder/cur/*; do docker exec -i $(docker-compose ps -q rspamd-mailcow) rspamc learn_ham < $file; done -# Spam -for file in /my/folder/.Junk/cur/*; do docker exec -i $(docker-compose ps -q rspamd-mailcow) rspamc learn_spam < $file; done -``` - -Consider attaching a local folder as new volume to `rspamd-mailcow` in `docker-compose.yml` and learn given files inside the container. This can be used as workaround to parse compressed data with zcat. Example: - -``` -for file in /data/old_mail/.Junk/cur/*; do rspamc learn_spam < zcat $file; done -``` - -### CLI tools - -``` -docker-compose exec rspamd-mailcow rspamc --help -docker-compose exec rspamd-mailcow rspamadm --help -``` - -See [Rspamd documentation](https://rspamd.com/doc/index.html) - -## Adjust service configurations - -The most important configuration files are mounted from the host into the related containers: - -``` -data/conf -├── bind9 -│   └── named.conf -├── dovecot -│   ├── dovecot.conf -│   ├── dovecot-master.passwd -│   ├── sieve_after -│   └── sql -│   ├── dovecot-dict-sql.conf -│   └── dovecot-mysql.conf -├── mysql -│   └── my.cnf -├── nginx -│   ├── dynmaps.conf -│   ├── site.conf -│   └── templates -│   ├── listen_plain.template -│   ├── listen_ssl.template -│   └── server_name.template -├── pdns -│   ├── pdns_custom.lua -│   └── recursor.conf -├── postfix -│   ├── main.cf -│   ├── master.cf -│   ├── postscreen_access.cidr -│   ├── smtp_dsn_filter -│   └── sql -│   ├── mysql_relay_recipient_maps.cf -│   ├── mysql_tls_enforce_in_policy.cf -│   ├── mysql_tls_enforce_out_policy.cf -│   ├── mysql_virtual_alias_domain_catchall_maps.cf -│   ├── mysql_virtual_alias_domain_maps.cf -│   ├── mysql_virtual_alias_maps.cf -│   ├── mysql_virtual_domains_maps.cf -│   ├── mysql_virtual_mailbox_maps.cf -│   ├── mysql_virtual_relay_domain_maps.cf -│   ├── mysql_virtual_sender_acl.cf -│   └── mysql_virtual_spamalias_maps.cf -├── rmilter -│   └── rmilter.conf -├── rspamd -│   ├── dynmaps -│   │   ├── authoritative.php -│   │   ├── settings.php -│   │   ├── tags.php -│   │   └── vars.inc.php -> ../../../web/inc/vars.inc.php -│   ├── local.d -│   │   ├── dkim.conf -│   │   ├── metrics.conf -│   │   ├── options.inc -│   │   ├── redis.conf -│   │   ├── rspamd.conf.local -│   │   └── statistic.conf -│   ├── lua -│   │   └── rspamd.local.lua -│   └── override.d -│   ├── logging.inc -│   ├── worker-controller.inc -│   └── worker-normal.inc -└── sogo - ├── sieve.creds - └── sogo.conf - -``` - -Just change the according configuration file on the host and restart the related service: -``` -docker-compose restart service-mailcow -``` - -## Tagging - -Mailbox users can tag their mail address like in `me+facebook@example.org` and choose between to setups to handle this tag: - -1\. Move this message to a subfolder "facebook" (will be created lower case if not existing) - -2\. Prepend the tag to the subject: "[facebook] Subject" - -## Two-factor authentication - -So far two methods for TFA are implemented. Both work with the fantastic [Yubikey](https://www.yubico.com). - -While Yubi OTP needs an active internet connection and an API ID and key, U2F will work with any FIDO U2F USB key out of the box, but can only be used when mailcow is accessed over HTTPS. - -Both methods support multiple YubiKeys. - -As administrator you are able to temporary disable a domain administrators TFA login until they successfully logged in. - -The key used to login will be displayed in green, while other keys remain grey. - -### Yubi OTP - -The Yubi API ID and Key will be checked against the Yubico Cloud API. When setting up TFA you will be asked for your personal API account for this key. -The API ID, API key and the first 12 characters (your YubiKeys ID in modhex) are stored in the MySQL table as secret. - -### U2F - -Only Google Chrome (+derivates) and Opera support U2F authentication to this day natively. -For Firefox you will need to install the "U2F Support Add-on" as provided on [mozilla.org](https://addons.mozilla.org/en-US/firefox/addon/u2f-support-add-on/). - -U2F works without an internet connection. - -## Portainer - -In order to enable Portainer, the docker-compose.yml and site.conf for nginx must be modified. - -1\. docker-compose.yml: Insert this block for portainer -``` - portainer-mailcow: - image: portainer/portainer - volumes: - - /var/run/docker.sock:/var/run/docker.sock - restart: always - dns: - - 172.22.1.254 - dns_search: mailcow-network - networks: - mailcow-network: - aliases: - - portainer -``` -2a\. data/conf/nginx/site.conf: Just beneath the opening line, at the same level as a server { block, add this: -``` -upstream portainer { - server portainer-mailcow:9000; -} - -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} -``` - -2b\. data/conf/nginx/site.conf: Then, inside **both** (ssl and plain) server blocks, add this: -``` - location /portainer/ { - proxy_http_version 1.1; - proxy_set_header Host $http_host; # required for docker client's sake - proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 900; - - proxy_set_header Connection ""; - proxy_buffers 32 4k; - proxy_pass http://portainer/; - } - - location /portainer/api/websocket/ { - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_pass http://portainer/api/websocket/; - } -``` - -Now you can simply navigate to https://${MAILCOW_HOSTNAME}/portainer/ to view your Portainer container monitoring page. You’ll then be prompted to specify a new password for the **admin** account. After specifying your password, you’ll then be able to connect to the Portainer UI. - -## Change autodiscover setup type - -This disables ActiveSync in the autodiscover service for Outlook and configures it with IMAP and SMTP instead: - -Open `data/web/autodiscover.php` and set `'useEASforOutlook' => 'yes'` to `'useEASforOutlook' => 'no'`. - -To always use IMAP and SMTP instead of EAS, set `'autodiscoverType' => 'imap'`. - -## Why Bind? - -For DNS blacklist lookups and DNSSEC. - -Most systems use either a public or a local caching DNS resolver. -That's a very bad idea when it comes to filter spam using DNS-based blackhole lists (DNSBL) or similar technics. -Most if not all providers apply a rate limit based on the DNS resolver that is used to query their service. -Using a public resolver like Googles 4x8, OpenDNS or any other shared DNS resolver like your ISPs will hit that limit very soon. From 67c2502eb68a9475a7304d94eac4e57869f1aeb1 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 1 May 2017 21:35:30 +0200 Subject: [PATCH 096/108] Remove old docs file, remove docs site ignore --- .gitignore | 1 - mkdocs.yml | 25 ------------------------- 2 files changed, 26 deletions(-) delete mode 100644 mkdocs.yml diff --git a/.gitignore b/.gitignore index 6ccd199d..c61097e9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,4 @@ data/conf/nginx/server_name.active data/conf/postfix/sql data/conf/dovecot/sql data/web/inc/vars.local.inc.php -site/ data/assets/ssl diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 1ab6bec8..00000000 --- a/mkdocs.yml +++ /dev/null @@ -1,25 +0,0 @@ -site_name: "mailcow: dockerized" -repo_url: https://github.com/andryyy/mailcow-dockerized -remote_branch: gh-pages -theme: material -extra: - social: - - type: 'github' - link: 'https://github.com/andryyy/mailcow-dockerized' - palette: - primary: 'indigo' - accent: 'yellow' - logo: 'images/logo.svg' - -markdown_extensions: - - admonition - - codehilite(guess_lang=true) - - footnotes - - meta - - toc(permalink=true) - -pages: - - 'This is mailcow': 'index.md' - - 'Installation': 'install.md' - - 'First Steps': 'first_steps.md' - - 'Usage & Examples': 'u_and_e.md' From 73be3bb259b7f6f6a53b95bbc4a29a8651d7579d Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 1 May 2017 21:36:07 +0200 Subject: [PATCH 097/108] Remove mkdocs.yml, remove unused docs site ignore --- .gitignore | 1 - mkdocs.yml | 25 ------------------------- 2 files changed, 26 deletions(-) delete mode 100644 mkdocs.yml diff --git a/.gitignore b/.gitignore index 6ccd199d..c61097e9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,4 @@ data/conf/nginx/server_name.active data/conf/postfix/sql data/conf/dovecot/sql data/web/inc/vars.local.inc.php -site/ data/assets/ssl diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 1ab6bec8..00000000 --- a/mkdocs.yml +++ /dev/null @@ -1,25 +0,0 @@ -site_name: "mailcow: dockerized" -repo_url: https://github.com/andryyy/mailcow-dockerized -remote_branch: gh-pages -theme: material -extra: - social: - - type: 'github' - link: 'https://github.com/andryyy/mailcow-dockerized' - palette: - primary: 'indigo' - accent: 'yellow' - logo: 'images/logo.svg' - -markdown_extensions: - - admonition - - codehilite(guess_lang=true) - - footnotes - - meta - - toc(permalink=true) - -pages: - - 'This is mailcow': 'index.md' - - 'Installation': 'install.md' - - 'First Steps': 'first_steps.md' - - 'Usage & Examples': 'u_and_e.md' From 4ff6f2dbf92902a4ae3066c6948415b253a54f00 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 1 May 2017 21:43:19 +0200 Subject: [PATCH 098/108] Fix readme link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 025dc825..29857408 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) -Please see [the official documentation](https://andryyy.github.io/mailcow-dockerized/) for instructions. +Please see [the official documentation](https://mailcow.github.io/mailcow-dockerized-docs/) for instructions. From 8f5ffdf020aac9f8eb184153222c01f4d5d27199 Mon Sep 17 00:00:00 2001 From: andryyy Date: Mon, 1 May 2017 21:43:41 +0200 Subject: [PATCH 099/108] Fix readme link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 025dc825..29857408 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) -Please see [the official documentation](https://andryyy.github.io/mailcow-dockerized/) for instructions. +Please see [the official documentation](https://mailcow.github.io/mailcow-dockerized-docs/) for instructions. From 3fc42465a016079ea4eb77a1ea921ec9c584301e Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 2 May 2017 09:42:07 +0200 Subject: [PATCH 100/108] Fix width problems in mailbox table, change image name to stretch --- data/Dockerfiles/postfix/Dockerfile | 2 +- data/web/js/mailbox.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 9da92ad6..210de532 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:testing-slim +FROM debian:stretch-slim MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js index 6119cdee..64816cfc 100644 --- a/data/web/js/mailbox.js +++ b/data/web/js/mailbox.js @@ -107,8 +107,8 @@ $(document).ready(function() { }); $('#mailbox_table').footable({ "columns": [ - {"sorted": true,"name":"username","style":{"word-break":"break-all"},"title":lang.username}, - {"name":"name","title":lang.fname,"style":{"word-break":"break-all"},"breakpoints":"xs sm"}, + {"sorted": true,"name":"username","style":{"word-break":"break-all","min-width":"180px"},"title":lang.username}, + {"name":"name","title":lang.fname,"style":{"word-break":"break-all","min-width":"180px"},"breakpoints":"xs sm"}, {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota,"formatter": function(value){ res = value.split("/"); From 24c97515a86d62ced23147e5b7d165caa4c82c2c Mon Sep 17 00:00:00 2001 From: andryyy Date: Tue, 2 May 2017 15:17:37 +0200 Subject: [PATCH 101/108] 80% table width on large screens, some minor td size changes --- data/web/js/mailbox.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js index 64816cfc..6acb22eb 100644 --- a/data/web/js/mailbox.js +++ b/data/web/js/mailbox.js @@ -107,8 +107,8 @@ $(document).ready(function() { }); $('#mailbox_table').footable({ "columns": [ - {"sorted": true,"name":"username","style":{"word-break":"break-all","min-width":"180px"},"title":lang.username}, - {"name":"name","title":lang.fname,"style":{"word-break":"break-all","min-width":"180px"},"breakpoints":"xs sm"}, + {"sorted": true,"name":"username","style":{"word-break":"break-all","min-width":"120px"},"title":lang.username}, + {"name":"name","title":lang.fname,"style":{"word-break":"break-all","min-width":"120px"},"breakpoints":"xs sm"}, {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota,"formatter": function(value){ res = value.split("/"); @@ -120,10 +120,10 @@ $(document).ready(function() { }, }, {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm md"}, - {"name":"in_use","filterable": false,"style":{"whiteSpace":"nowrap"},"type":"html","title":lang.in_use}, - {"name":"messages","filterable": false,"style":{"whiteSpace":"nowrap"},"title":lang.msg_num,"breakpoints":"xs sm md"}, - {"name":"active","filterable": false,"style":{"whiteSpace":"nowrap"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"whiteSpace":"nowrap","text-align":"right","width":"290px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} + {"name":"in_use","filterable": false,"type":"html","title":lang.in_use}, + {"name":"messages","filterable": false,"title":lang.msg_num,"breakpoints":"xs sm md"}, + {"name":"active","filterable": false,"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","min-width":"250px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} ], "empty": lang.empty, "rows": data, From 0d9cdce81835017e8178de8e9788966d740c8a99 Mon Sep 17 00:00:00 2001 From: broedli Date: Tue, 2 May 2017 17:35:45 +0200 Subject: [PATCH 102/108] Adjust 'save changes' button save buttons are green everywhere else ... --- data/web/user.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/user.php b/data/web/user.php index 03e1a22b..711f9d80 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -398,7 +398,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
- +
From e80ecd8a4c4c0d6ca63b6fe8958f8265eada435a Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 3 May 2017 18:02:04 +0200 Subject: [PATCH 103/108] Show correct timezone in sync jobs --- data/web/user.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/web/user.php b/data/web/user.php index 03e1a22b..2e923d08 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -45,6 +45,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
@@ -431,7 +432,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' ' . $row['exclude'] . '';?> min - + Date: Wed, 3 May 2017 18:05:13 +0200 Subject: [PATCH 104/108] Hopefully fix all Nginx reverse proxy issues, see documentation updates! --- data/conf/nginx/site.conf | 58 ++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/data/conf/nginx/site.conf b/data/conf/nginx/site.conf index 685e1fc7..edd57d32 100644 --- a/data/conf/nginx/site.conf +++ b/data/conf/nginx/site.conf @@ -16,6 +16,7 @@ server { include /etc/nginx/conf.d/server_name.active; error_log /var/log/nginx/error.log; access_log /var/log/nginx/access.log; + absolute_redirect off; root /web; location ~ ^/api/v1/(.*)$ { @@ -33,7 +34,7 @@ server { real_ip_recursive on; location = /principals/ { - rewrite ^ $scheme://$host:$server_port/SOGo/dav; + rewrite ^ $scheme://$http_host/SOGo/dav; allow all; } @@ -51,16 +52,12 @@ server { fastcgi_read_timeout 1200; } - rewrite ^(/save.+)$ /rspamd$1 last; location /rspamd/ { proxy_pass http://172.22.1.253:11334/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; - add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; - add_header X-Content-Type-Options nosniff; - add_header X-Frame-Options SAMEORIGIN; - add_header X-XSS-Protection "1; mode=block"; + proxy_redirect off; } location ~ /(?:a|A)utodiscover/(?:a|A)utodiscover.xml { @@ -91,11 +88,11 @@ server { proxy_busy_buffers_size 64k; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header x-webobjects-server-protocol HTTP/1.0; proxy_set_header x-webobjects-remote-host $remote_addr; proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; + proxy_set_header x-webobjects-server-url $scheme://$http_host; proxy_set_header x-webobjects-server-port $server_port; client_body_buffer_size 128k; client_max_body_size 100m; @@ -105,11 +102,11 @@ server { proxy_pass http://172.22.1.252:20000; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header x-webobjects-server-protocol HTTP/1.0; proxy_set_header x-webobjects-remote-host $remote_addr; proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; + proxy_set_header x-webobjects-server-url $scheme://$http_host; proxy_set_header x-webobjects-server-port $server_port; client_body_buffer_size 128k; client_max_body_size 100m; @@ -118,7 +115,7 @@ server { location /SOGo.woa/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -128,7 +125,7 @@ server { location /.woa/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -138,7 +135,7 @@ server { location /SOGo/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -148,7 +145,7 @@ server { location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$ { proxy_pass http://172.22.1.252:9192/$1.SOGo/Resources/$2; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -164,6 +161,7 @@ server { include /etc/nginx/conf.d/server_name.active; error_log /var/log/nginx/error.log; access_log /var/log/nginx/access.log; + absolute_redirect off; root /web; location ~ ^/api/v1/(.*)$ { @@ -181,7 +179,7 @@ server { real_ip_recursive on; location = /principals/ { - rewrite ^ $scheme://$host:$server_port/SOGo/dav; + rewrite ^ $scheme://$http_host/SOGo/dav; allow all; } @@ -199,20 +197,12 @@ server { fastcgi_read_timeout 1200; } - rewrite ^(/save.+)$ /rspamd$1 last; location /rspamd/ { proxy_pass http://172.22.1.253:11334/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; - add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; - add_header X-Content-Type-Options nosniff; - add_header X-Frame-Options SAMEORIGIN; - add_header X-XSS-Protection "1; mode=block"; - } - - location ^~ /inc/init.sql { - deny all; + proxy_redirect off; } location ~ /(?:a|A)utodiscover/(?:a|A)utodiscover.xml { @@ -243,11 +233,11 @@ server { proxy_busy_buffers_size 64k; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header x-webobjects-server-protocol HTTP/1.0; proxy_set_header x-webobjects-remote-host $remote_addr; proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; + proxy_set_header x-webobjects-server-url $scheme://$http_host; proxy_set_header x-webobjects-server-port $server_port; client_body_buffer_size 128k; client_max_body_size 100m; @@ -257,11 +247,11 @@ server { proxy_pass http://172.22.1.252:20000; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header x-webobjects-server-protocol HTTP/1.0; proxy_set_header x-webobjects-remote-host $remote_addr; proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; + proxy_set_header x-webobjects-server-url $scheme://$http_host; proxy_set_header x-webobjects-server-port $server_port; client_body_buffer_size 128k; client_max_body_size 100m; @@ -270,7 +260,7 @@ server { location /SOGo.woa/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -280,7 +270,7 @@ server { location /.woa/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -290,7 +280,7 @@ server { location /SOGo/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -300,7 +290,7 @@ server { location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$ { proxy_pass http://172.22.1.252:9192/$1.SOGo/Resources/$2; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; From f1e4b4fb39c4001a7bd0d9088e6ab5851c71f00e Mon Sep 17 00:00:00 2001 From: andryyy Date: Wed, 3 May 2017 18:05:35 +0200 Subject: [PATCH 105/108] Added TOTP, minor fixes --- data/web/admin.php | 1 + data/web/css/mailbox.css | 5 + data/web/css/mailcow.css | 1 + data/web/inc/footer.inc.php | 44 +- data/web/inc/functions.inc.php | 63 +- data/web/inc/init_db.inc.php | 4 +- data/web/inc/lib/composer.json | 6 + data/web/inc/lib/composer.lock | 100 ++ data/web/inc/lib/vendor/autoload.php | 7 + .../inc/lib/vendor/composer/ClassLoader.php | 445 +++++++ data/web/inc/lib/vendor/composer/LICENSE | 21 + .../lib/vendor/composer/autoload_classmap.php | 14 + .../vendor/composer/autoload_namespaces.php | 9 + .../inc/lib/vendor/composer/autoload_psr4.php | 10 + .../inc/lib/vendor/composer/autoload_real.php | 52 + .../lib/vendor/composer/autoload_static.php | 40 + .../inc/lib/vendor/composer/installed.json | 88 ++ .../vendor/robthree/twofactorauth/.gitignore | 186 +++ .../vendor/robthree/twofactorauth/.travis.yml | 11 + .../.vs/config/applicationhost.config | 1031 +++++++++++++++++ .../lib/vendor/robthree/twofactorauth/LICENSE | 22 + .../vendor/robthree/twofactorauth/README.md | 197 ++++ .../twofactorauth/TwoFactorAuth.phpproj | 69 ++ .../robthree/twofactorauth/TwoFactorAuth.sln | 22 + .../robthree/twofactorauth/composer.json | 36 + .../robthree/twofactorauth/composer.lock | 980 ++++++++++++++++ .../robthree/twofactorauth/demo/demo.php | 35 + .../robthree/twofactorauth/demo/loader.php | 50 + .../Providers/Qr/BaseHTTPQRCodeProvider.php | 27 + .../lib/Providers/Qr/GoogleQRCodeProvider.php | 39 + .../lib/Providers/Qr/IQRCodeProvider.php | 9 + .../lib/Providers/Qr/QRException.php | 5 + .../lib/Providers/Qr/QRServerProvider.php | 71 ++ .../lib/Providers/Qr/QRicketProvider.php | 54 + .../lib/Providers/Rng/CSRNGProvider.php | 14 + .../lib/Providers/Rng/HashRNGProvider.php | 28 + .../lib/Providers/Rng/IRNGProvider.php | 9 + .../lib/Providers/Rng/MCryptRNGProvider.php | 23 + .../lib/Providers/Rng/OpenSSLRNGProvider.php | 25 + .../lib/Providers/Rng/RNGException.php | 5 + .../ConvertUnixTimeDotComTimeProvider.php | 15 + .../lib/Providers/Time/HttpTimeProvider.php | 53 + .../lib/Providers/Time/ITimeProvider.php | 8 + .../Time/LocalMachineTimeProvider.php | 9 + .../lib/Providers/Time/TimeException.php | 5 + .../twofactorauth/lib/TwoFactorAuth.php | 249 ++++ .../lib/TwoFactorAuthException.php | 7 + .../vendor/robthree/twofactorauth/logo.png | Bin 0 -> 2636 bytes .../multifactorauthforeveryone.png | Bin 0 -> 20735 bytes .../twofactorauth/tests/TwoFactorAuthTest.php | 381 ++++++ .../vendor/yubico/u2flib-server/.gitignore | 7 + .../vendor/yubico/u2flib-server/.travis.yml | 19 + .../inc/lib/vendor/yubico/u2flib-server/BLURB | 9 + .../lib/vendor/yubico/u2flib-server/COPYING | 26 + .../inc/lib/vendor/yubico/u2flib-server/NEWS | 24 + .../lib/vendor/yubico/u2flib-server/README | 34 + .../vendor/yubico/u2flib-server/README.adoc | 1 + .../vendor/yubico/u2flib-server/apigen.neon | 12 + .../vendor/yubico/u2flib-server/composer.json | 13 + .../yubico/u2flib-server/do-source-release.sh | 40 + .../u2flib-server/examples/assets/u2f-api.js | 651 +++++++++++ .../u2flib-server/examples/cli/u2f-server.php | 83 ++ .../examples/localstorage/index.php | 186 +++ .../u2flib-server/examples/pdo/index.php | 204 ++++ .../vendor/yubico/u2flib-server/phpunit.xml | 9 + .../u2flib-server/src/u2flib_server}/U2F.php | 0 .../tests/certs/yubico-u2f-ca-1.pem | 19 + .../u2flib-server/tests/u2flib_test.php | 296 +++++ data/web/inc/prerequisites.inc.php | 5 +- data/web/inc/tfa_modals.php | 55 +- data/web/lang/lang.de.php | 5 + data/web/lang/lang.en.php | 5 + 72 files changed, 6261 insertions(+), 27 deletions(-) create mode 100644 data/web/inc/lib/composer.json create mode 100644 data/web/inc/lib/composer.lock create mode 100644 data/web/inc/lib/vendor/autoload.php create mode 100644 data/web/inc/lib/vendor/composer/ClassLoader.php create mode 100644 data/web/inc/lib/vendor/composer/LICENSE create mode 100644 data/web/inc/lib/vendor/composer/autoload_classmap.php create mode 100644 data/web/inc/lib/vendor/composer/autoload_namespaces.php create mode 100644 data/web/inc/lib/vendor/composer/autoload_psr4.php create mode 100644 data/web/inc/lib/vendor/composer/autoload_real.php create mode 100644 data/web/inc/lib/vendor/composer/autoload_static.php create mode 100644 data/web/inc/lib/vendor/composer/installed.json create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/.gitignore create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/.travis.yml create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/.vs/config/applicationhost.config create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/LICENSE create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/README.md create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.phpproj create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.sln create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/composer.json create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/composer.lock create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/demo/demo.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/demo/loader.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/BaseHTTPQRCodeProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/GoogleQRCodeProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/IQRCodeProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRException.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRServerProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRicketProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/CSRNGProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/HashRNGProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/IRNGProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/MCryptRNGProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/OpenSSLRNGProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/RNGException.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ITimeProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/LocalMachineTimeProvider.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/TimeException.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/TwoFactorAuth.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/lib/TwoFactorAuthException.php create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/logo.png create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/multifactorauthforeveryone.png create mode 100644 data/web/inc/lib/vendor/robthree/twofactorauth/tests/TwoFactorAuthTest.php create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/.gitignore create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/.travis.yml create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/BLURB create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/COPYING create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/NEWS create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/README create mode 120000 data/web/inc/lib/vendor/yubico/u2flib-server/README.adoc create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/apigen.neon create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/composer.json create mode 100755 data/web/inc/lib/vendor/yubico/u2flib-server/do-source-release.sh create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/examples/assets/u2f-api.js create mode 100755 data/web/inc/lib/vendor/yubico/u2flib-server/examples/cli/u2f-server.php create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/examples/localstorage/index.php create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/examples/pdo/index.php create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/phpunit.xml rename data/web/inc/lib/{ => vendor/yubico/u2flib-server/src/u2flib_server}/U2F.php (100%) create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/tests/certs/yubico-u2f-ca-1.pem create mode 100644 data/web/inc/lib/vendor/yubico/u2flib-server/tests/u2flib_test.php diff --git a/data/web/admin.php b/data/web/admin.php index ca20af8a..6da0f396 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -67,6 +67,7 @@ $tfa_data = get_tfa(); diff --git a/data/web/css/mailbox.css b/data/web/css/mailbox.css index 4a110ba1..2e6c1afe 100644 --- a/data/web/css/mailbox.css +++ b/data/web/css/mailbox.css @@ -34,3 +34,8 @@ table.footable>tbody>tr.footable-empty>td { #alias_table .footable-paging { cursor: auto; } +@media (min-width: 992px) { + .container { + width: 80%; + } +} \ No newline at end of file diff --git a/data/web/css/mailcow.css b/data/web/css/mailcow.css index f7db5296..20dfb69a 100644 --- a/data/web/css/mailcow.css +++ b/data/web/css/mailcow.css @@ -55,3 +55,4 @@ body.modal-open { overflow: inherit; padding-right: inherit !important; } + diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 3a3f9009..97b59c0e 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -93,6 +93,10 @@ $(document).ready(function() { $('#YubiOTPModal').modal('show'); $("option:selected").prop("selected", false); } + if ($(this).val() == "totp") { + $('#TOTPModal').modal('show'); + $("option:selected").prop("selected", false); + } if ($(this).val() == "u2f") { $('#U2FModal').modal('show'); $("option:selected").prop("selected", false); @@ -141,25 +145,27 @@ $(document).ready(function() { // Remember last navigation pill (function () { 'use strict'; - $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { - var id = $(this).parents('[role="tablist"]').attr('id'); - var key = 'lastTag'; - if (id) { - key += ':' + id; - } - localStorage.setItem(key, $(e.target).attr('href')); - }); - $('[role="tablist"]').each(function (idx, elem) { - var id = $(elem).attr('id'); - var key = 'lastTag'; - if (id) { - key += ':' + id; - } - var lastTab = localStorage.getItem(key); - if (lastTab) { - $('[href="' + lastTab + '"]').tab('show'); - } - }); + if ($('a[data-toggle="tab"]').length) { + $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { + var id = $(this).parents('[role="tablist"]').attr('id'); + var key = 'lastTag'; + if (id) { + key += ':' + id; + } + localStorage.setItem(key, $(e.target).attr('href')); + }); + $('[role="tablist"]').each(function (idx, elem) { + var id = $(elem).attr('id'); + var key = 'lastTag'; + if (id) { + key += ':' + id; + } + var lastTab = localStorage.getItem(key); + if (lastTab) { + $('[href="' + lastTab + '"]').tab('show'); + } + }); + } })(); // Disable submit after submitting form diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 12fd08c5..573a1a0b 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1754,6 +1754,7 @@ function set_tfa($postarray) { global $pdo; global $yubi; global $u2f; + global $tfa; if ($_SESSION['mailcow_cc_role'] != "domainadmin" && $_SESSION['mailcow_cc_role'] != "admin") { @@ -1850,6 +1851,36 @@ function set_tfa($postarray) { 'msg' => "U2F: " . $e->getMessage() ); $_SESSION['regReq'] = null; + return false; + } + break; + + case "totp": + (!isset($postarray["key_id"])) ? $key_id = 'unidentified' : $key_id = $postarray["key_id"]; + if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) { + try { + $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')"); + $stmt->execute(array($username, $key_id, $_POST['totp_secret'])); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['object_modified'], $username) + ); + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'TOTP verification failed' + ); } break; @@ -1970,8 +2001,16 @@ function get_tfa($username = null) { case "totp": $data['name'] = "totp"; $data['pretty'] = "Time-based OTP"; + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $data['additional'][] = $row; + } return $data; - break; + break; default: $data['name'] = 'none'; $data['pretty'] = "-"; @@ -1983,6 +2022,8 @@ function verify_tfa_login($username, $token) { global $pdo; global $lang; global $yubi; + global $u2f; + global $tfa; $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` WHERE `username` = :username AND `active` = '1'"); @@ -2020,7 +2061,6 @@ function verify_tfa_login($username, $token) { break; case "u2f": try { - global $u2f; $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), get_u2f_registrations($username), json_decode($token)); $stmt = $pdo->prepare("UPDATE `tfa` SET `counter` = ? WHERE `id` = ?"); $stmt->execute(array($reg->counter, $reg->id)); @@ -2042,7 +2082,26 @@ function verify_tfa_login($username, $token) { return false; break; case "totp": + try { + $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` + WHERE `username` = :username + AND `authmech` = 'totp' + AND `active`='1'"); + $stmt->execute(array(':username' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($tfa->verifyCode($row['secret'], $_POST['token']) === true) { + $_SESSION['tfa_id'] = $row['id']; + return true; + } return false; + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } break; default: return false; diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index d856f26b..81c9be2a 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -94,7 +94,7 @@ function init_db_schema() { "aliases" => "INT(10) NOT NULL DEFAULT '0'", "mailboxes" => "INT(10) NOT NULL DEFAULT '0'", "maxquota" => "BIGINT(20) NOT NULL DEFAULT '0'", - "quota" => "BIGINT(20) NOT NULL DEFAULT '0'", + "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", "transport" => "VARCHAR(255) NOT NULL", "backupmx" => "TINYINT(1) NOT NULL DEFAULT '0'", "relay_all_recipients" => "TINYINT(1) NOT NULL DEFAULT '0'", @@ -177,7 +177,7 @@ function init_db_schema() { "password" => "VARCHAR(255) NOT NULL", "name" => "VARCHAR(255)", "maildir" => "VARCHAR(255) NOT NULL", - "quota" => "BIGINT(20) NOT NULL DEFAULT '0'", + "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", "local_part" => "VARCHAR(255) NOT NULL", "domain" => "VARCHAR(255) NOT NULL", "tls_enforce_in" => "TINYINT(1) NOT NULL DEFAULT '0'", diff --git a/data/web/inc/lib/composer.json b/data/web/inc/lib/composer.json new file mode 100644 index 00000000..d811958b --- /dev/null +++ b/data/web/inc/lib/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "robthree/twofactorauth": "^1.6", + "yubico/u2flib-server": "^1.0" + } +} diff --git a/data/web/inc/lib/composer.lock b/data/web/inc/lib/composer.lock new file mode 100644 index 00000000..692521d2 --- /dev/null +++ b/data/web/inc/lib/composer.lock @@ -0,0 +1,100 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "5652a086b6d277d72d7ae0341e517b1e", + "packages": [ + { + "name": "robthree/twofactorauth", + "version": "1.6", + "source": { + "type": "git", + "url": "https://github.com/RobThree/TwoFactorAuth.git", + "reference": "5093ab230cd8f1296d792afb6a49545f37e7fd5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/5093ab230cd8f1296d792afb6a49545f37e7fd5a", + "reference": "5093ab230cd8f1296d792afb6a49545f37e7fd5a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "@stable" + }, + "type": "library", + "autoload": { + "psr-4": { + "RobThree\\Auth\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Janssen", + "homepage": "http://robiii.me", + "role": "Developer" + } + ], + "description": "Two Factor Authentication", + "homepage": "https://github.com/RobThree/TwoFactorAuth", + "keywords": [ + "Authentication", + "MFA", + "Multi Factor Authentication", + "Two Factor Authentication", + "authenticator", + "authy", + "php", + "tfa" + ], + "time": "2017-02-17T15:24:54+00:00" + }, + { + "name": "yubico/u2flib-server", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/Yubico/php-u2flib-server.git", + "reference": "407eb21da24150aad30bcd8cc0ee72963eac5e9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yubico/php-u2flib-server/zipball/407eb21da24150aad30bcd8cc0ee72963eac5e9d", + "reference": "407eb21da24150aad30bcd8cc0ee72963eac5e9d", + "shasum": "" + }, + "require": { + "ext-openssl": "*" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Library for U2F implementation", + "homepage": "https://developers.yubico.com/php-u2flib-server", + "time": "2016-02-19T09:47:51+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/data/web/inc/lib/vendor/autoload.php b/data/web/inc/lib/vendor/autoload.php new file mode 100644 index 00000000..011c5277 --- /dev/null +++ b/data/web/inc/lib/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath.'\\'; + if (isset($this->prefixDirsPsr4[$search])) { + foreach ($this->prefixDirsPsr4[$search] as $dir) { + $length = $this->prefixLengthsPsr4[$first][$search]; + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/data/web/inc/lib/vendor/composer/LICENSE b/data/web/inc/lib/vendor/composer/LICENSE new file mode 100644 index 00000000..f27399a0 --- /dev/null +++ b/data/web/inc/lib/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/data/web/inc/lib/vendor/composer/autoload_classmap.php b/data/web/inc/lib/vendor/composer/autoload_classmap.php new file mode 100644 index 00000000..44393069 --- /dev/null +++ b/data/web/inc/lib/vendor/composer/autoload_classmap.php @@ -0,0 +1,14 @@ + $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\RegisterRequest' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\Registration' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\SignRequest' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\U2F' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php', +); diff --git a/data/web/inc/lib/vendor/composer/autoload_namespaces.php b/data/web/inc/lib/vendor/composer/autoload_namespaces.php new file mode 100644 index 00000000..b7fc0125 --- /dev/null +++ b/data/web/inc/lib/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/robthree/twofactorauth/lib'), +); diff --git a/data/web/inc/lib/vendor/composer/autoload_real.php b/data/web/inc/lib/vendor/composer/autoload_real.php new file mode 100644 index 00000000..caf455f3 --- /dev/null +++ b/data/web/inc/lib/vendor/composer/autoload_real.php @@ -0,0 +1,52 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit873464e4bd965a3168f133248b1b218b::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/data/web/inc/lib/vendor/composer/autoload_static.php b/data/web/inc/lib/vendor/composer/autoload_static.php new file mode 100644 index 00000000..5e2dabab --- /dev/null +++ b/data/web/inc/lib/vendor/composer/autoload_static.php @@ -0,0 +1,40 @@ + + array ( + 'RobThree\\Auth\\' => 14, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'RobThree\\Auth\\' => + array ( + 0 => __DIR__ . '/..' . '/robthree/twofactorauth/lib', + ), + ); + + public static $classMap = array ( + 'u2flib_server\\Error' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\RegisterRequest' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\Registration' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\SignRequest' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\U2F' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit873464e4bd965a3168f133248b1b218b::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit873464e4bd965a3168f133248b1b218b::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit873464e4bd965a3168f133248b1b218b::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/data/web/inc/lib/vendor/composer/installed.json b/data/web/inc/lib/vendor/composer/installed.json new file mode 100644 index 00000000..bbe76d82 --- /dev/null +++ b/data/web/inc/lib/vendor/composer/installed.json @@ -0,0 +1,88 @@ +[ + { + "name": "robthree/twofactorauth", + "version": "1.6", + "version_normalized": "1.6.0.0", + "source": { + "type": "git", + "url": "https://github.com/RobThree/TwoFactorAuth.git", + "reference": "5093ab230cd8f1296d792afb6a49545f37e7fd5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/5093ab230cd8f1296d792afb6a49545f37e7fd5a", + "reference": "5093ab230cd8f1296d792afb6a49545f37e7fd5a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "@stable" + }, + "time": "2017-02-17T15:24:54+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "RobThree\\Auth\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Janssen", + "homepage": "http://robiii.me", + "role": "Developer" + } + ], + "description": "Two Factor Authentication", + "homepage": "https://github.com/RobThree/TwoFactorAuth", + "keywords": [ + "Authentication", + "MFA", + "Multi Factor Authentication", + "Two Factor Authentication", + "authenticator", + "authy", + "php", + "tfa" + ] + }, + { + "name": "yubico/u2flib-server", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/Yubico/php-u2flib-server.git", + "reference": "407eb21da24150aad30bcd8cc0ee72963eac5e9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yubico/php-u2flib-server/zipball/407eb21da24150aad30bcd8cc0ee72963eac5e9d", + "reference": "407eb21da24150aad30bcd8cc0ee72963eac5e9d", + "shasum": "" + }, + "require": { + "ext-openssl": "*" + }, + "time": "2016-02-19T09:47:51+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Library for U2F implementation", + "homepage": "https://developers.yubico.com/php-u2flib-server" + } +] diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/.gitignore b/data/web/inc/lib/vendor/robthree/twofactorauth/.gitignore new file mode 100644 index 00000000..1a31666a --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/.gitignore @@ -0,0 +1,186 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +#NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# If using the old MSBuild-Integrated Package Restore, uncomment this: +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Composer +/vendor \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/.travis.yml b/data/web/inc/lib/vendor/robthree/twofactorauth/.travis.yml new file mode 100644 index 00000000..034653bb --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/.travis.yml @@ -0,0 +1,11 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7 + - hhvm + +script: phpunit --coverage-text tests diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/.vs/config/applicationhost.config b/data/web/inc/lib/vendor/robthree/twofactorauth/.vs/config/applicationhost.config new file mode 100644 index 00000000..4b9bf477 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/.vs/config/applicationhost.config @@ -0,0 +1,1031 @@ + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/LICENSE b/data/web/inc/lib/vendor/robthree/twofactorauth/LICENSE new file mode 100644 index 00000000..75a96312 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2015 Rob Janssen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/README.md b/data/web/inc/lib/vendor/robthree/twofactorauth/README.md new file mode 100644 index 00000000..d65eb199 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/README.md @@ -0,0 +1,197 @@ +# ![Logo](https://raw.githubusercontent.com/RobThree/TwoFactorAuth/master/logo.png) PHP library for Two Factor Authentication + +[![Build status](https://img.shields.io/travis/RobThree/TwoFactorAuth.svg?style=flat-square)](https://travis-ci.org/RobThree/TwoFactorAuth/) [![Latest Stable Version](https://img.shields.io/packagist/v/robthree/twofactorauth.svg?style=flat-square)](https://packagist.org/packages/robthree/twofactorauth) [![License](https://img.shields.io/packagist/l/robthree/twofactorauth.svg?style=flat-square)](LICENSE) [![Downloads](https://img.shields.io/packagist/dt/robthree/twofactorauth.svg?style=flat-square)](https://packagist.org/packages/robthree/twofactorauth) [![HHVM Status](https://img.shields.io/hhvm/RobThree/TwoFactorAuth.svg?style=flat-square)](http://hhvm.h4cc.de/package/robthree/twofactorauth) [![Code Climate](https://img.shields.io/codeclimate/github/RobThree/TwoFactorAuth.svg?style=flat-square)](https://codeclimate.com/github/RobThree/TwoFactorAuth) [![PayPal donate button](http://img.shields.io/badge/paypal-donate-orange.svg?style=flat-square)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6MB5M2SQLP636 "Keep me off the streets") + +PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedia.org/wiki/Multi-factor_authentication) using [TOTP](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) and [QR-codes](http://en.wikipedia.org/wiki/QR_code). Inspired by, based on but most importantly an *improvement* on '[PHPGangsta/GoogleAuthenticator](https://github.com/PHPGangsta/GoogleAuthenticator)'. There's a [.Net implementation](https://github.com/RobThree/TwoFactorAuth.Net) of this library as well. + +

+ +

+ +## Requirements + +* Tested on PHP 5.3, 5.4, 5.5 and 5.6, 7 and HHVM +* [cURL](http://php.net/manual/en/book.curl.php) when using the provided `GoogleQRCodeProvider` (default), `QRServerProvider` or `QRicketProvider` but you can also provide your own QR-code provider. +* [random_bytes()](http://php.net/manual/en/function.random-bytes.php), [MCrypt](http://php.net/manual/en/book.mcrypt.php), [OpenSSL](http://php.net/manual/en/book.openssl.php) or [Hash](http://php.net/manual/en/book.hash.php) depending on which built-in RNG you use (TwoFactorAuth will try to 'autodetect' and use the best available); however: feel free to provide your own (CS)RNG. + +## Installation + +Run the following command: + +`php composer.phar require robthree/twofactorauth` + +## Quick start + +If you want to hit the ground running then have a look at the [demo](demo/demo.php). It's very simple and easy! + +## Usage + +Here are some code snippets that should help you get started... + +````php +// Create a TwoFactorAuth instance +$tfa = new RobThree\Auth\TwoFactorAuth('My Company'); +```` + +The TwoFactorAuth class constructor accepts 7 parameters (all optional): + +Parameter | Default value | Use +------------------|---------------|-------------------------------------------------- +`$issuer` | `null` | Will be displayed in the app as issuer name +`$digits` | `6` | The number of digits the resulting codes will be +`$period` | `30` | The number of seconds a code will be valid +`$algorithm` | `sha1` | The algorithm used +`$qrcodeprovider` | `null` | QR-code provider (more on this later) +`$rngprovider` | `null` | Random Number Generator provider (more on this later) +`$timeprovider` | `null` | Time provider (more on this later) + +These parameters are all '`write once`'; the class will, for it's lifetime, use these values when generating / calculating codes. The number of digits, the period and algorithm are all set to values Google's Authticator app uses (and supports). You may specify `8` digits, a period of `45` seconds and the `sha256` algorithm but the authenticator app (be it Google's implementation, Authy or any other app) may or may not support these values. Your mileage may vary; keep it on the safe side if you don't control which app your audience uses. + +### Step 1: Set up secret shared key + +When a user wants to setup two-factor auth (or, more correctly, multi-factor auth) you need to create a secret. This will be your **shared secret**. This secret will need to be entered by the user in their app. This can be done manually, in which case you simply display the secret and have the user type it in the app: + +````php +$secret = $tfa->createSecret(); +```` + +The `createSecret()` method accepts two arguments: `$bits` (default: `80`) and `$requirecryptosecure` (default: `true`). The former is the number of bits generated for the shared secret. Make sure this argument is a multiple of 8 and, again, keep in mind that not all combinations may be supported by all apps. Google authenticator seems happy with 80 and 160, the default is set to 80 because that's what most sites (that I know of) currently use; however a value of 160 or higher is recommended (see [RFC 4226 - Algorithm Requirements](https://tools.ietf.org/html/rfc4226#section-4)). The latter is used to ensure that the secret is cryptographically secure; if you don't care very much for cryptographically secure secrets you can specify `false` and use a **non**-cryptographically secure RNG provider. + +````php +// Display shared secret +

Please enter the following code in your app: ''

+```` + +Another, more user-friendly, way to get the shared secret into the app is to generate a [QR-code](http://en.wikipedia.org/wiki/QR_code) which can be scanned by the app. To generate these QR codes you can use any one of the built-in `QRProvider` classes: + +1. `GoogleQRCodeProvider` (default) +2. `QRServerProvider` +3. `QRicketProvider` + +...or implement your own provider. To implement your own provider all you need to do is implement the `IQRCodeProvider` interface. You can use the built-in providers mentioned before to serve as an example or read the next chapter in this file. The built-in classes all use a 3rd (e.g. external) party (Google, QRServer and QRicket) for the hard work of generating QR-codes (note: each of these services might at some point not be available or impose limitations to the number of codes generated per day, hour etc.). You could, however, easily use a project like [PHP QR Code](http://phpqrcode.sourceforge.net/) (or one of the [many others](https://packagist.org/search/?q=qr)) to generate your QR-codes without depending on external sources. Later on we'll [demonstrate](#qr-code-providers) how to do this. + +The built-in providers all have some provider-specific 'tweaks' you can 'apply'. Some provide support for different colors, others may let you specify the desired image-format etc. What they all have in common is that they return a QR-code as binary blob which, in turn, will be turned into a [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme) by the `TwoFactorAuth` class. This makes it easy for you to display the image without requiring extra 'roundtrips' from browser to server and vice versa. + +````php +// Display QR code to user +

Scan the following image with your app:

+

+```` + +When outputting a QR-code you can choose a `$label` for the user (which, when entering a shared secret manually, will have to be chosen by the user). This label may be an empty string or `null`. Also a `$size` may be specified (in pixels, width == height) for which we use a default value of `200`. + +### Step 2: Verify secret shared key + +When the shared secret is added to the app, the app will be ready to start generating codes which 'expire' each '`$period`' number of seconds. To make sure the secret was entered, or scanned, correctly you need to verify this by having the user enter a generated code. To check if the generated code is valid you call the `verifyCode()` method: + +````php +// Verify code +$result = $tfa->verifyCode($_SESSION['secret'], $_POST['verification']); +```` + +`verifyCode()` will return either `true` (the code was valid) or `false` (the code was invalid; no points for you!). You may need to store `$secret` in a `$_SESSION` or other persistent storage between requests. The `verifyCode()` accepts, aside from `$secret` and `$code`, two more parameters. The first being `$discrepancy`. Since TOTP codes are based on time("slices") it is very important that the server (but also client) have a correct date/time. But because the two *may* differ a bit we usually allow a certain amount of leeway. Because generated codes are valid for a specific period (remember the `$period` parameter in the `TwoFactorAuth`'s constructor?) we usually check the period directly before and the period directly after the current time when validating codes. So when the current time is `14:34:21`, which results in a 'current timeslice' of `14:34:00` to `14:34:30` we also calculate/verify the codes for `14:33:30` to `14:34:00` and for `14:34:30` to `14:35:00`. This gives us a 'window' of `14:33:30` to `14:35:00`. The `$discrepancy` parameter specifies how many periods (or: timeslices) we check in either direction of the current time. The default `$discrepancy` of `1` results in (max.) 3 period checks: -1, current and +1 period. A `$discrepancy` of `4` would result in a larger window (or: bigger time difference between client and server) of -4, -3, -2, -1, current, +1, +2, +3 and +4 periods. + +The second parameter `$time` allows you to check a code for a specific point in time. This parameter has no real practical use but can be handy for unittesting etc. The default value, `null`, means: use the current time. + +### Step 3: Store `$secret` with user and we're done! + +Ok, so now the code has been verified and found to be correct. Now we can store the `$secret` with our user in our database (or elsewhere) and whenever the user begins a new session we ask for a code generated by the authentication app of their choice. All we need to do is call `verifyCode()` again with the shared secret and the entered code and we know if the user is legit or not. + +Simple as 1-2-3. + +All we need is 3 methods and a constructor: + +````php +public function __construct( + $issuer = null, + $digits = 6, + $period = 30, + $algorithm = 'sha1', + RobThree\Auth\Providers\Qr\IQRCodeProvider $qrcodeprovider = null, + RobThree\Auth\Providers\Rng\IRNGProvider $rngprovider = null +); +public function createSecret($bits = 80, $requirecryptosecure = true): string; +public function getQRCodeImageAsDataUri($label, $secret, $size = 200): string; +public function verifyCode($secret, $code, $discrepancy = 1, $time = null): bool; +```` + +### QR-code providers + +As mentioned before, this library comes with three 'built-in' QR-code providers. This chapter will touch the subject a bit but most of it should be self-explanatory. The `TwoFactorAuth`-class accepts a `$qrcodeprovider` parameter which lets you specify a built-in or custom QR-code provider. All three built-in providers do a simple HTTP request to retrieve an image using cURL and implement the [`IQRCodeProvider`](lib/Providers/Qr/IQRCodeProvider.php) interface which is all you need to implement to write your own QR-code provider. + +The default provider is the [`GoogleQRCodeProvider`](lib/Providers/Qr/GoogleQRCodeProvider.php) which uses the [Google Chart Tools](https://developers.google.com/chart/infographics/docs/qr_codes) to render QR-codes. Then we have the [`QRServerProvider`](lib/Providers/Qr/QRServerProvider.php) which uses the [goqr.me API](http://goqr.me/api/doc/create-qr-code/) and finally we have the [`QRicketProvider`](lib/Providers/Qr/QRicketProvider.php) which uses the [QRickit API](http://qrickit.com/qrickit_apps/qrickit_api.php). All three inherit from a common (abstract) baseclass named [`BaseHTTPQRCodeProvider`](lib/Providers/Qr/BaseHTTPQRCodeProvider.php) because all three share the same functionality: retrieve an image from a 3rd party over HTTP. All three classes have constructors that allow you to tweak some settings and most, if not all, arguments should speak for themselves. If you're not sure which values are supported, click the links in this paragraph for documentation on the API's that are utilized by these classes. + +If you don't like any of the built-in classes because you don't want to rely on external resources for example or because you're paranoid about sending the TOTP secret to these 3rd parties (which is useless to them since they miss *at least one* other factor in the [MFA process](http://en.wikipedia.org/wiki/Multi-factor_authentication)), feel tree to implement your own. The `IQRCodeProvider` interface couldn't be any simpler. All you need to do is implement 2 methods: + +````php +getMimeType(); +getQRCodeImage($qrtext, $size); +```` + +The `getMimeType()` method should return the [MIME type](http://en.wikipedia.org/wiki/Internet_media_type) of the image that is returned by our implementation of `getQRCodeImage()`. In this example it's simply `image/png`. The `getQRCodeImage()` method is passed two arguments: `$qrtext` and `$size`. The latter, `$size`, is simply the width/height in pixels of the image desired by the caller. The first, `$qrtext` is the text that should be encoded in the QR-code. An example of such a text would be: + +`otpauth://totp/LABEL:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=ISSUER` + +All you need to do is return the QR-code as binary image data and you're done. All parts of the `$qrtext` have been escaped for you (but note: you *may* need to escape the entire `$qrtext` just once more when passing the data to another server as GET-parameter). + +Let's see if we can use [PHP QR Code](http://phpqrcode.sourceforge.net/) to implement our own, custom, no-3rd-parties-allowed-here, provider. We start with downloading the [required (single) file](https://github.com/t0k4rt/phpqrcode/blob/master/phpqrcode.php) and putting it in the directory where `TwoFactorAuth.php` is located as well. Now let's implement the provider: create another file named `myprovider.php` in the `Providers\Qr` directory and paste in this content: + +````php +createSecret(); +?> +

+```` + +Voilà. Couldn't make it any simpler. + +### RNG providers + +This library also comes with three 'built-in' RNG providers ([Random Number Generator](https://en.wikipedia.org/wiki/Random_number_generation)). The RNG provider generates a number of random bytes and returns these bytes as a string. These values are then used to create the secret. By default (no RNG provider specified) TwoFactorAuth will try to determine the best available RNG provider to use. It will, by default, try to use the [`CSRNGProvider`](lib/Providers/Rng/CSRNGProvider.php) for PHP7+ or the [`MCryptRNGProvider`](lib/Providers/Rng/MCryptRNGProvider.php); if this is not available/supported for any reason it will try to use the [`OpenSSLRNGProvider`](lib/Providers/Rng/OpenSSLRNGProvider.php) and if that is also not available/supported it will try to use the final RNG provider: [`HashRNGProvider`](lib/Providers/Rng/HashRNGProvider.php). Each of these providers use their own method of generating a random sequence of bytes. The first three (`CSRNGProvider`, `OpenSSLRNGProvider` and `MCryptRNGProvider`) return a [cryptographically secure](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) sequence of random bytes whereas the `HashRNGProvider` returns a **non-cryptographically secure** sequence. + +You can easily implement your own `RNGProvider` by simply implementing the `IRNGProvider` interface. Each of the 'built-in' RNG providers have some constructor parameters that allow you to 'tweak' some of the settings to use when creating the random bytes such as which source to use (`MCryptRNGProvider`) or which hashing algorithm (`HashRNGProvider`). I encourage you to have a look at some of the ['built-in' RNG providers](lib/Providers/Rng) for details and the [`IRNGProvider` interface](lib/Providers/Rng/IRNGProvider.php). + +### Time providers + +Another set of providers in this library are the Time Providers; this library provides three 'built-in' ones. The default Time Provider used is the [`LocalMachineTimeProvider`](lib/Providers/Time/LocalMachineTimeProvider.php); this provider simply returns the output of `Time()` and is *highly recommended* as default provider. The [`HttpTimeProvider`](lib/Providers/Time/HttpTimeProvider.php) executes a `HEAD` request against a given webserver (default: google.com) and tries to extract the `Date:`-HTTP header and returns it's date. Other url's/domains can be used by specifying the url in the constructor. The final Time Provider is the [`ConvertUnixTimeDotComTimeProvider`](lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php) which does a HTTP request to `convert-unix-time.com/api` and decodes the `JSON` result to retrieve the time. + +You can easily implement your own `TimeProvider` by simply implementing the `ITimeProvider` interface. + +As to *why* these Time Providers are implemented: it allows the TwoFactorAuth library to ensure the hosts time is correct (or rather: within a margin). You can use the `ensureCorrectTime()` method to ensure the hosts time is correct. By default this method will compare the hosts time (returned by calling `time()` on the `LocalMachineTimeProvider`) to Google's and convert-unix-time.com's current time. You can pass an array of `ITimeProvider`s and specify the `leniency` (second argument) allowed (default: 5 seconds). The method will throw when the TwoFactorAuth's timeprovider (which can be any `ITimeProvider`, see constructor) differs more than the given amount of seconds from any of the given `ITimeProviders`. We advise to call this method sparingly when relying on 3rd parties (which both the `HttpTimeProvider` and `ConvertUnixTimeDotComTimeProvider` do) or, if you need to ensure time is correct on a (very) regular basis to implement an `ITimeProvider` that is more efficient than the 'built-in' ones (like use a GPS signal). The `ensureCorrectTime()` method is mostly to be used to make sure the server is configured correctly. + +## Integrations + +- [CakePHP 3](https://github.com/andrej-griniuk/cakephp-two-factor-auth) + +## License + +Licensed under MIT license. See [LICENSE](https://raw.githubusercontent.com/RobThree/TwoFactorAuth/master/LICENSE) for details. + +[Logo / icon](http://www.iconmay.com/Simple/Travel_and_Tourism_Part_2/luggage_lock_safety_baggage_keys_cylinder_lock_hotel_travel_tourism_luggage_lock_icon_465) under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication ([Archived page](http://riii.nl/tm7ap)) diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.phpproj b/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.phpproj new file mode 100644 index 00000000..7fa2a58e --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.phpproj @@ -0,0 +1,69 @@ + + + + Debug + TwoFactorAuth + {e569f53a-a604-4579-91ce-4e35b27da47b} + TwoFactorAuth + Library + {A0786B88-2ADB-4C21-ABE8-AA2D79766269} + False + PHPDev + None + True + 41315 + localhost + http://localhost:41315/ + PHP + 7.0 + + + true + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.sln b/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.sln new file mode 100644 index 00000000..df901f6a --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30723.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{A0786B88-2ADB-4C21-ABE8-AA2D79766269}") = "TwoFactorAuth", "TwoFactorAuth.phpproj", "{E569F53A-A604-4579-91CE-4E35B27DA47B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E569F53A-A604-4579-91CE-4E35B27DA47B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E569F53A-A604-4579-91CE-4E35B27DA47B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E569F53A-A604-4579-91CE-4E35B27DA47B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E569F53A-A604-4579-91CE-4E35B27DA47B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/composer.json b/data/web/inc/lib/vendor/robthree/twofactorauth/composer.json new file mode 100644 index 00000000..a4c13758 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/composer.json @@ -0,0 +1,36 @@ +{ + "name": "robthree/twofactorauth", + "description": "Two Factor Authentication", + "version": "1.6", + "type": "library", + "keywords": [ "Authentication", "Two Factor Authentication", "Multi Factor Authentication", "TFA", "MFA", "PHP", "Authenticator", "Authy" ], + "homepage": "https://github.com/RobThree/TwoFactorAuth", + "license": "MIT", + "authors": [ + { + "name": "Rob Janssen", + "homepage": "http://robiii.me", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/RobThree/TwoFactorAuth/issues", + "source": "https://github.com/RobThree/TwoFactorAuth" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "@stable" + }, + "autoload": { + "psr-4": { + "RobThree\\Auth\\": "lib" + } + }, + "autoload-dev": { + "psr-4": { + "RobThree\\Auth\\Test\\": "tests" + } + } +} diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/composer.lock b/data/web/inc/lib/vendor/robthree/twofactorauth/composer.lock new file mode 100644 index 00000000..63df9377 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/composer.lock @@ -0,0 +1,980 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "9647de85f54ba6db237f5ff42ff85a1f", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14T21:17:01+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2015-02-03T12:10:50+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6c52c2722f8460122f96f86346600e1077ce22cb", + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1", + "sebastian/recursion-context": "^1.0|^2.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.0", + "phpunit/phpunit": "^4.8 || ^5.6.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2016-11-21T14:58:47+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "2.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "~1.3", + "sebastian/environment": "^1.3.2", + "sebastian/version": "~1.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~4" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2015-10-06T15:47:00+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2016-10-03T07:40:28+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2016-05-12T18:03:57+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3b402f65a4cc90abf6e1104e388b896ce209631b", + "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2016-11-15T14:06:22+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "4.8.35", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "791b1a67c25af50e230f841ee7a9c6eba507dc87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/791b1a67c25af50e230f841ee7a9c6eba507dc87", + "reference": "791b1a67c25af50e230f841ee7a9c6eba507dc87", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "~2.1", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.2.2", + "sebastian/diff": "~1.2", + "sebastian/environment": "~1.3", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/version": "~1.0", + "symfony/yaml": "~2.1|~3.0" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.8.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2017-02-06T05:18:07+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "2.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": ">=5.3.3", + "phpunit/php-text-template": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2015-10-02T06:51:40+00:00" + }, + { + "name": "sebastian/comparator", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2 || ~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2017-01-29T09:50:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-12-08T07:14:41+00:00" + }, + { + "name": "sebastian/environment", + "version": "1.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-08-18T05:49:44+00:00" + }, + { + "name": "sebastian/exporter", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-06-17T09:04:28+00:00" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12T03:26:01+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-11-11T19:50:13+00:00" + }, + { + "name": "sebastian/version", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2015-06-21T13:59:46+00:00" + }, + { + "name": "symfony/yaml", + "version": "v2.8.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "322a8c2dfbca15ad6b1b27e182899f98ec0e0153" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/322a8c2dfbca15ad6b1b27e182899f98ec0e0153", + "reference": "322a8c2dfbca15ad6b1b27e182899f98ec0e0153", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2017-01-21T16:40:50+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "phpunit/phpunit": 0 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.3.0" + }, + "platform-dev": [] +} diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/demo/demo.php b/data/web/inc/lib/vendor/robthree/twofactorauth/demo/demo.php new file mode 100644 index 00000000..996dd928 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/demo/demo.php @@ -0,0 +1,35 @@ + + + + Demo + + +
    + First create a secret and associate it with a user'; + $secret = $tfa->createSecret(160); // Though the default is an 80 bits secret (for backwards compatibility reasons) we recommend creating 160+ bits secrets (see RFC 4226 - Algorithm Requirements) + echo '
  1. Next create a QR code and let the user scan it:

    ...or display the secret to the user for manual entry: ' . chunk_split($secret, 4, ' '); + $code = $tfa->getCode($secret); + echo '
  2. Next, have the user verify the code; at this time the code displayed by a 2FA-app would be: ' . $code . ' (but that changes periodically)'; + echo '
  3. When the code checks out, 2FA can be / is enabled; store (encrypted?) secret with user and have the user verify a code each time a new session is started.'; + echo '
  4. When aforementioned code (' . $code . ') was entered, the result would be: ' . (($tfa->verifyCode($secret, $code) === true) ? 'OK' : 'FAIL'); + ?> +
+

Note: Make sure your server-time is NTP-synced! Depending on the $discrepancy allowed your time cannot drift too much from the users' time!

+ ensureCorrectTime(); + echo 'Your hosts time seems to be correct / within margin'; + } catch (RobThree\Auth\TwoFactorAuthException $ex) { + echo 'Warning: Your hosts time seems to be off: ' . $ex->getMessage(); + } + ?> + + diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/demo/loader.php b/data/web/inc/lib/vendor/robthree/twofactorauth/demo/loader.php new file mode 100644 index 00000000..208f24d4 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/demo/loader.php @@ -0,0 +1,50 @@ +=0;$i--) { + static::$parentPath = dirname(static::$parentPath); + } + static::$paths = array(); + static::$files = array(__FILE__); + } + + public static function register($path,$namespace) { + if (!static::$initialized) static::initialize(); + static::$paths[$namespace] = trim($path,DIRECTORY_SEPARATOR); + } + + public static function load($class) { + if (class_exists($class,false)) return; + if (!static::$initialized) static::initialize(); + + foreach (static::$paths as $namespace => $path) { + if (!$namespace || $namespace.static::$nsChar === substr($class, 0, strlen($namespace.static::$nsChar))) { + + $fileName = substr($class,strlen($namespace.static::$nsChar)-1); + $fileName = str_replace(static::$nsChar, DIRECTORY_SEPARATOR, ltrim($fileName,static::$nsChar)); + $fileName = static::$parentPath.DIRECTORY_SEPARATOR.$path.DIRECTORY_SEPARATOR.$fileName.'.php'; + + if (file_exists($fileName)) { + include $fileName; + return true; + } + } + } + return false; + } +} + +spl_autoload_register(array('Loader', 'load')); \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/BaseHTTPQRCodeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/BaseHTTPQRCodeProvider.php new file mode 100644 index 00000000..5cb3adda --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/BaseHTTPQRCodeProvider.php @@ -0,0 +1,27 @@ + $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_DNS_CACHE_TIMEOUT => 10, + CURLOPT_TIMEOUT => 10, + CURLOPT_SSL_VERIFYPEER => $this->verifyssl, + CURLOPT_USERAGENT => 'TwoFactorAuth' + )); + $data = curl_exec($curlhandle); + + curl_close($curlhandle); + return $data; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/GoogleQRCodeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/GoogleQRCodeProvider.php new file mode 100644 index 00000000..19e086b7 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/GoogleQRCodeProvider.php @@ -0,0 +1,39 @@ +verifyssl = $verifyssl; + + $this->errorcorrectionlevel = $errorcorrectionlevel; + $this->margin = $margin; + } + + public function getMimeType() + { + return 'image/png'; + } + + public function getQRCodeImage($qrtext, $size) + { + return $this->getContent($this->getUrl($qrtext, $size)); + } + + public function getUrl($qrtext, $size) + { + return 'https://chart.googleapis.com/chart?cht=qr' + . '&chs=' . $size . 'x' . $size + . '&chld=' . $this->errorcorrectionlevel . '|' . $this->margin + . '&chl=' . rawurlencode($qrtext); + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/IQRCodeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/IQRCodeProvider.php new file mode 100644 index 00000000..83ed67ba --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/IQRCodeProvider.php @@ -0,0 +1,9 @@ +verifyssl = $verifyssl; + + $this->errorcorrectionlevel = $errorcorrectionlevel; + $this->margin = $margin; + $this->qzone = $qzone; + $this->bgcolor = $bgcolor; + $this->color = $color; + $this->format = $format; + } + + public function getMimeType() + { + switch (strtolower($this->format)) + { + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + case 'jpg': + case 'jpeg': + return 'image/jpeg'; + case 'svg': + return 'image/svg+xml'; + case 'eps': + return 'application/postscript'; + } + throw new \QRException(sprintf('Unknown MIME-type: %s', $this->format)); + } + + public function getQRCodeImage($qrtext, $size) + { + return $this->getContent($this->getUrl($qrtext, $size)); + } + + private function decodeColor($value) + { + return vsprintf('%d-%d-%d', sscanf($value, "%02x%02x%02x")); + } + + public function getUrl($qrtext, $size) + { + return 'https://api.qrserver.com/v1/create-qr-code/' + . '?size=' . $size . 'x' . $size + . '&ecc=' . strtoupper($this->errorcorrectionlevel) + . '&margin=' . $this->margin + . '&qzone=' . $this->qzone + . '&bgcolor=' . $this->decodeColor($this->bgcolor) + . '&color=' . $this->decodeColor($this->color) + . '&format=' . strtolower($this->format) + . '&data=' . rawurlencode($qrtext); + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRicketProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRicketProvider.php new file mode 100644 index 00000000..59e27ccd --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRicketProvider.php @@ -0,0 +1,54 @@ +verifyssl = false; + + $this->errorcorrectionlevel = $errorcorrectionlevel; + $this->bgcolor = $bgcolor; + $this->color = $color; + $this->format = $format; + } + + public function getMimeType() + { + switch (strtolower($this->format)) + { + case 'p': + return 'image/png'; + case 'g': + return 'image/gif'; + case 'j': + return 'image/jpeg'; + } + throw new \QRException(sprintf('Unknown MIME-type: %s', $this->format)); + } + + public function getQRCodeImage($qrtext, $size) + { + return $this->getContent($this->getUrl($qrtext, $size)); + } + + public function getUrl($qrtext, $size) + { + return 'http://qrickit.com/api/qr' + . '?qrsize=' . $size + . '&e=' . strtolower($this->errorcorrectionlevel) + . '&bgdcolor=' . $this->bgcolor + . '&fgdcolor=' . $this->color + . '&t=' . strtolower($this->format) + . '&d=' . rawurlencode($qrtext); + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/CSRNGProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/CSRNGProvider.php new file mode 100644 index 00000000..8dba7fc9 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/CSRNGProvider.php @@ -0,0 +1,14 @@ +algorithm = $algorithm; + } + + public function getRandomBytes($bytecount) { + $result = ''; + $hash = mt_rand(); + for ($i = 0; $i < $bytecount; $i++) { + $hash = hash($this->algorithm, $hash.mt_rand(), true); + $result .= $hash[mt_rand(0, sizeof($hash))]; + } + return $result; + } + + public function isCryptographicallySecure() { + return false; + } +} diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/IRNGProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/IRNGProvider.php new file mode 100644 index 00000000..6be28006 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/IRNGProvider.php @@ -0,0 +1,9 @@ +source = $source; + } + + public function getRandomBytes($bytecount) { + $result = mcrypt_create_iv($bytecount, $this->source); + if ($result === false) + throw new \RNGException('mcrypt_create_iv returned an invalid value'); + return $result; + } + + public function isCryptographicallySecure() { + return true; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/OpenSSLRNGProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/OpenSSLRNGProvider.php new file mode 100644 index 00000000..dc66c64a --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/OpenSSLRNGProvider.php @@ -0,0 +1,25 @@ +requirestrong = $requirestrong; + } + + public function getRandomBytes($bytecount) { + $result = openssl_random_pseudo_bytes($bytecount, $crypto_strong); + if ($this->requirestrong && ($crypto_strong === false)) + throw new \RNGException('openssl_random_pseudo_bytes returned non-cryptographically strong value'); + if ($result === false) + throw new \RNGException('openssl_random_pseudo_bytes returned an invalid value'); + return $result; + } + + public function isCryptographicallySecure() { + return $this->requirestrong; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/RNGException.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/RNGException.php new file mode 100644 index 00000000..eb5e913d --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/RNGException.php @@ -0,0 +1,5 @@ +timestamp)) + throw new \TimeException('Unable to retrieve time from convert-unix-time.com'); + return $json->timestamp; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php new file mode 100644 index 00000000..c761bd97 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php @@ -0,0 +1,53 @@ +url = $url; + $this->expectedtimeformat = $expectedtimeformat; + $this->options = $options; + if ($this->options === null) { + $this->options = array( + 'http' => array( + 'method' => 'HEAD', + 'follow_location' => false, + 'ignore_errors' => true, + 'max_redirects' => 0, + 'request_fulluri' => true, + 'header' => array( + 'Connection: close', + 'User-agent: TwoFactorAuth HttpTimeProvider (https://github.com/RobThree/TwoFactorAuth)' + ) + ) + ); + } + } + + public function getTime() { + try { + $context = stream_context_create($this->options); + $fd = fopen($this->url, 'rb', false, $context); + $headers = stream_get_meta_data($fd); + fclose($fd); + + foreach ($headers['wrapper_data'] as $h) { + if (strcasecmp(substr($h, 0, 5), 'Date:') === 0) + return \DateTime::createFromFormat($this->expectedtimeformat, trim(substr($h,5)))->getTimestamp(); + } + throw new \TimeException(sprintf('Unable to retrieve time from %s (Invalid or no "Date:" header found)', $this->url)); + } + catch (Exception $ex) { + throw new \TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->url, $ex->getMessage())); + } + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ITimeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ITimeProvider.php new file mode 100644 index 00000000..a3b87a20 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ITimeProvider.php @@ -0,0 +1,8 @@ +issuer = $issuer; + if (!is_int($digits) || $digits <= 0) + throw new TwoFactorAuthException('Digits must be int > 0'); + $this->digits = $digits; + + if (!is_int($period) || $period <= 0) + throw new TwoFactorAuthException('Period must be int > 0'); + $this->period = $period; + + $algorithm = strtolower(trim($algorithm)); + if (!in_array($algorithm, self::$_supportedalgos)) + throw new TwoFactorAuthException('Unsupported algorithm: ' . $algorithm); + $this->algorithm = $algorithm; + $this->qrcodeprovider = $qrcodeprovider; + $this->rngprovider = $rngprovider; + $this->timeprovider = $timeprovider; + + self::$_base32 = str_split(self::$_base32dict); + self::$_base32lookup = array_flip(self::$_base32); + } + + /** + * Create a new secret + */ + public function createSecret($bits = 80, $requirecryptosecure = true) + { + $secret = ''; + $bytes = ceil($bits / 5); //We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32) + $rngprovider = $this->getRngprovider(); + if ($requirecryptosecure && !$rngprovider->isCryptographicallySecure()) + throw new TwoFactorAuthException('RNG provider is not cryptographically secure'); + $rnd = $rngprovider->getRandomBytes($bytes); + for ($i = 0; $i < $bytes; $i++) + $secret .= self::$_base32[ord($rnd[$i]) & 31]; //Mask out left 3 bits for 0-31 values + return $secret; + } + + /** + * Calculate the code with given secret and point in time + */ + public function getCode($secret, $time = null) + { + $secretkey = $this->base32Decode($secret); + + $timestamp = "\0\0\0\0" . pack('N*', $this->getTimeSlice($this->getTime($time))); // Pack time into binary string + $hashhmac = hash_hmac($this->algorithm, $timestamp, $secretkey, true); // Hash it with users secret key + $hashpart = substr($hashhmac, ord(substr($hashhmac, -1)) & 0x0F, 4); // Use last nibble of result as index/offset and grab 4 bytes of the result + $value = unpack('N', $hashpart); // Unpack binary value + $value = $value[1] & 0x7FFFFFFF; // Drop MSB, keep only 31 bits + + return str_pad($value % pow(10, $this->digits), $this->digits, '0', STR_PAD_LEFT); + } + + /** + * Check if the code is correct. This will accept codes starting from ($discrepancy * $period) sec ago to ($discrepancy * period) sec from now + */ + public function verifyCode($secret, $code, $discrepancy = 1, $time = null) + { + $result = false; + $timetamp = $this->getTime($time); + + // To keep safe from timing-attachs we iterate *all* possible codes even though we already may have verified a code is correct + for ($i = -$discrepancy; $i <= $discrepancy; $i++) + $result |= $this->codeEquals($this->getCode($secret, $timetamp + ($i * $this->period)), $code); + + return (bool)$result; + } + + /** + * Timing-attack safe comparison of 2 codes (see http://blog.ircmaxell.com/2014/11/its-all-about-time.html) + */ + private function codeEquals($safe, $user) { + if (function_exists('hash_equals')) { + return hash_equals($safe, $user); + } + // In general, it's not possible to prevent length leaks. So it's OK to leak the length. The important part is that + // we don't leak information about the difference of the two strings. + if (strlen($safe)===strlen($user)) { + $result = 0; + for ($i = 0; $i < strlen($safe); $i++) + $result |= (ord($safe[$i]) ^ ord($user[$i])); + return $result === 0; + } + return false; + } + + /** + * Get data-uri of QRCode + */ + public function getQRCodeImageAsDataUri($label, $secret, $size = 200) + { + if (!is_int($size) || $size <= 0) + throw new TwoFactorAuthException('Size must be int > 0'); + + $qrcodeprovider = $this->getQrCodeProvider(); + return 'data:' + . $qrcodeprovider->getMimeType() + . ';base64,' + . base64_encode($qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size)); + } + + /** + * Compare default timeprovider with specified timeproviders and ensure the time is within the specified number of seconds (leniency) + */ + public function ensureCorrectTime(array $timeproviders = null, $leniency = 5) + { + if ($timeproviders != null && !is_array($timeproviders)) + throw new TwoFactorAuthException('No timeproviders specified'); + + if ($timeproviders == null) + $timeproviders = array( + new Providers\Time\ConvertUnixTimeDotComTimeProvider(), + new Providers\Time\HttpTimeProvider() + ); + + // Get default time provider + $timeprovider = $this->getTimeProvider(); + + // Iterate specified time providers + foreach ($timeproviders as $t) { + if (!($t instanceof ITimeProvider)) + throw new TwoFactorAuthException('Object does not implement ITimeProvider'); + + // Get time from default time provider and compare to specific time provider and throw if time difference is more than specified number of seconds leniency + if (abs($timeprovider->getTime() - $t->getTime()) > $leniency) + throw new TwoFactorAuthException(sprintf('Time for timeprovider is off by more than %d seconds when compared to %s', $leniency, get_class($t))); + } + } + + private function getTime($time) + { + return ($time === null) ? $this->getTimeProvider()->getTime() : $time; + } + + private function getTimeSlice($time = null, $offset = 0) + { + return (int)floor($time / $this->period) + ($offset * $this->period); + } + + /** + * Builds a string to be encoded in a QR code + */ + public function getQRText($label, $secret) + { + return 'otpauth://totp/' . rawurlencode($label) + . '?secret=' . rawurlencode($secret) + . '&issuer=' . rawurlencode($this->issuer) + . '&period=' . intval($this->period) + . '&algorithm=' . rawurlencode(strtoupper($this->algorithm)) + . '&digits=' . intval($this->digits); + } + + private function base32Decode($value) + { + if (strlen($value)==0) return ''; + + if (preg_match('/[^'.preg_quote(self::$_base32dict).']/', $value) !== 0) + throw new TwoFactorAuthException('Invalid base32 string'); + + $buffer = ''; + foreach (str_split($value) as $char) + { + if ($char !== '=') + $buffer .= str_pad(decbin(self::$_base32lookup[$char]), 5, 0, STR_PAD_LEFT); + } + $length = strlen($buffer); + $blocks = trim(chunk_split(substr($buffer, 0, $length - ($length % 8)), 8, ' ')); + + $output = ''; + foreach (explode(' ', $blocks) as $block) + $output .= chr(bindec(str_pad($block, 8, 0, STR_PAD_RIGHT))); + return $output; + } + + /** + * @return IQRCodeProvider + * @throws TwoFactorAuthException + */ + public function getQrCodeProvider() + { + // Set default QR Code provider if none was specified + if (null === $this->qrcodeprovider) { + return $this->qrcodeprovider = new Providers\Qr\GoogleQRCodeProvider(); + } + return $this->qrcodeprovider; + } + + /** + * @return IRNGProvider + * @throws TwoFactorAuthException + */ + public function getRngprovider() + { + if (null !== $this->rngprovider) { + return $this->rngprovider; + } + if (function_exists('random_bytes')) { + return $this->rngprovider = new Providers\Rng\CSRNGProvider(); + } + if (function_exists('mcrypt_create_iv')) { + return $this->rngprovider = new Providers\Rng\MCryptRNGProvider(); + } + if (function_exists('openssl_random_pseudo_bytes')) { + return $this->rngprovider = new Providers\Rng\OpenSSLRNGProvider(); + } + if (function_exists('hash')) { + return $this->rngprovider = new Providers\Rng\HashRNGProvider(); + } + throw new TwoFactorAuthException('Unable to find a suited RNGProvider'); + } + + /** + * @return ITimeProvider + * @throws TwoFactorAuthException + */ + public function getTimeProvider() + { + // Set default time provider if none was specified + if (null === $this->timeprovider) { + return $this->timeprovider = new Providers\Time\LocalMachineTimeProvider(); + } + return $this->timeprovider; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/TwoFactorAuthException.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/TwoFactorAuthException.php new file mode 100644 index 00000000..af51b748 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/TwoFactorAuthException.php @@ -0,0 +1,7 @@ +=2C00000001Z^Bme*ahK7I%2?_P}^~A!$jg5x_0s;aE2Ib@9=H%o!J2&R# z<^eO;!oR+~)goAyEi-+9V+0xI?wX&}?J~|#!MS*{N zB34QT77?kZrxr~>8AUt8zP@m7YdmFJfq#5KNk--5guDQ zpsTBp2^B^=7E3=zMMd}a_a`SN zRbN~aM>`cvKNCnh6G=Q3NlaT#^z`)c@bMNFDO?+u7O9%gXTX@8I6v6;44DMLGBO_ZL}87gk3SLpTO2A@lO{-Q3#|K{f>~ zBIV@d*w)t8)6>w*%*@Hk2QMY|_4e-V?&IR&)6vn!!@~|dF&t@E6iYt}H7e53&kQ*( z($3Dz$jHOMz=wo|9A#7qF(>-^`oO%r3^ptVDImzj#UOEB1SlQ^CLF!GyRfgXtgEaa zZCe^%P6QztwzIUTr>D=(&7GQ=mXni!eSRQnSsGzc8#gbcp`@appN@=;gn)r%V`dps zN4mJTrn0Ykc6c0DN&+Pt#LCFR!^D)PqL7Y~ZjXmSbZ9knWmQyJ{QUfYmyutKfVsiH zwZ6QXuBmi>er;=UU20`PWLqUkKv95oGe|;No6ki600512QchC<0t6rm7!zv@Ed&B; zQVchn)y&Jsy|k^XrJI(Ngn@i?bZk&YLpw12 z`|s=P=jYws(b3Jz%g4jNy}h}$m6L~h5UIM$000JeNklnaU%ZTtTy=*NKa zVS%QiV@z>AyxPI;d{aK9lS#_%{5ClznAPe6Dc(-&)e9-9VE zV3z<~7B3eF(6bL_mSnfHqhJz)trku*KF%AJR>JLVmjFf)9|$lCO;ia>~fzeFvcvAcl)unS`b$O5xtNx2Z{ANL^Ou?bcvY3^50qW)eCo ztr*Iy$3nY7q;TH%_f=zITn?*?gV{tu+49}C^F3Ty;o*#&{P$%=Y|;`l0sN>uxE=yp z^x;I}A{qROs`u}`)w9heDlM1a8*96ue`+i)JeCnf&Mhk|dV3XXoTv{K06RJ; z@}VRmQ(ac_MuuOZ&rD=6RgxL?5x@_}MJ6(0qF5dBU&&O<2p=tPC8k%_V`KoE)M ziP5Sm&AWoa>gsCxmG%whw)x;i^sSEoC{cHTXhD_ccZLX0M_4htGY)2X*Gs^CTv}u( z>3$SN>isbzqmWJ@5X#HT!#h(#f%nZA&18tyAcd#XZM+A9908^UvN~x5AygAfMPIU`dz%&TR z%l@KNiU@%Qr~ss4dYuARaSQ@99dZx>fwfp!SHWa*sO|#Mie7?G$_#v9NV1W76bw`` z^b-I}d9T0(P(v&Uq!I|5t>;@WBpL-|y)4L(g#{y+M#Kux$^#ar->~|R= zLa+rPf%MGG@_~)5|AIyV#InV85m?i zGRWgEw6Jbq3dPXNDz`m0QYPqxg5fINN;5MY&AvHF)-Rvs$A zG9N?$7g!7gSm(N4fLJO(1z6^(HUt1p=>$+-OWufNw4ggEHU*ey8HoyDX|gK|K|U@n z?v9CI0pEC&x#` zn-LdGpwp={2c6~dctY2{t*sP<5~HH|MXEP6T401FabShRHoLi|u-O7mituQUf6rkI z1?Io>t_-77Ba%#je0(r9jCy*OtANcBr*OGkVW$?Jp0#~8iRP(9LVtpTE2iy!u?p=D;vV0P-nZ zvZ5$EcX{I`L;4@7c@wvy@0xE(Ne>_Bc>EyYPY#iEpBT;JQbM6;NO-E%K0{eF@O&qs5P}easi_bI z2_-wx0#IYgywT}83`LK?l|CMX`%u6o$dH1};~{y8M6wsX0{E(btdmo7XJ<#pp0hXl+|!}e_GaCuEFX8n-@h5kwd25m*V+IV_wBm^oSYmm z<^@hzXUCo{gS!XJX|`*^Xdj<+pJmG~PdImGLR&yrK-;zh02{k*Z|EN|*rlhVGuFHT zgB}2>03F@u?D6;Wb9ZlO#NFN9&(F`l*`7H8-MbHVLF3KQBbY!9IDwkxSS;2V;8+W1 u02bT4Ify!eFJsK(mO2eMn1Rt#JM|ZOJX2v|M^Y~U0000H7O3`uiKj#l-*r@{r#2d40`(eO8fmQv$C&WUR`$%F#rD+ac^z} z0|MRL+G%EDLpLOe1VdL%L9Pr}Dj^h2ML8lC4j~``{}YG$`h|ysdjJ10(GG3C3Sc`i zCg51m!ove&WCGm?a8C*$ z%E`v??@eT0Fo=OH+1b=pPccnJEQNeF^#y$A=SI@cHva#G<>XPL2v675OQoGOV_YHO z;6d*1Gdcns<>jl;&qJb~KeMebYGp@iI2TV)0N2)<_V;jlF*C)&H!dqF%F2}B-;$l1 zXM=uL^75{)t%~8|0HdP-z`u~cy>XCbE5N+5pLl*%F&@#<0PgOSt*U6JjxcmiDBId| zk&J1IOC`$4QS_3Vs>i}!YO9RL7Ly-7qtRCwC#nz4@J zFc5~VUv6kuvLYG^aO4+QD@96_;tHrzrCubsdjzB{Xznfcy5lwPoW%YOhTTJ(+sc2j zn{mc7&KK;AAtEa)D=RB2D=RB2D=RB2D=RB2D=X_GZ7PiF+TTK)f>~`;H6AJFM}zGQ zk*EqCtf0!>GVzN7EO#Y+k9FI@>20slu5s|~djB@|ua8L{hWNB9P^~zD(mYgRbD$Bo z3&B&=W%r2I4v8uu&a`&K+JQQslCBKC*Pyyxc_+_Cd|E&l9y-xNd2<2C;Yea8zF{7W zxc#r(DT>_}!@&v#TbFpc@+ZR^Eda$l)Ewt&V3C6nUk@v~L(gd4 zzu(=MgA5AXhSeq6C7^DRH8@QOOQD7QdRV7uQ7bjvWNCd9)!cza&xlrgk%yNzA*>P| z>c?UIgps|*D1%YF4@-J!R8a9`Il0TyrN2Vigfp_&%AatWLq7#iMdBe({nv??Hz6!h z0L{a&_%QQ}^q}`)A;>@rN}&?MLUIt0J`Mny^I+1`jEE&+6|R|=Iw35}aX$h}sM%i; z*3y~le}c8#sl5wFX(&-(Hh+&hJ6$k6~S`< zK7!@7^VeIKW6-_mP?DU5kqZFBqd$W6EgrpF>oy1Wi(oBQ9^u=Ruo~1~1&f81pCv~5 zOZtu?4s{+%@NXNDz;d1h!B6A@mwEhcmjhIIgy!17bBhW2YgdkJe(p|a|ISPJF?SWN*V zrh+B=zkTAVVX+RA39L{jSwMvX=3;W4{Z=@I^%Ew>YM>HW%-oif!7_{+V9oy@EIG;{ zqC7SoRYq>-xRL;%ZmwDM6ye#(a=-RPmVptI!9w6}0xTOQ#&(A~g%$1SAB6=keWTJ2 z7j1AljyuOjqD^4&xoKd899n;$>7b26Kf6> z{IU!stA@1 z`5PQI6Tuosp(0qq0J6L+R>Xv`YS_?+t9@@8VBQeG0(eP^(!kjgS2G9NGKTft&1v@e zlq&dwr|@gAl}-+T*aLtZQ~=ly803{SeuY31J0bcKXfWp&KUf5(D*h(S_I(rzhOh@_ z2A~I4Om6Vg*JT|58$1uT&%=?uK#zvI|DTe=s1}2C=NtOd?P&71udPNR!$5vM`*_i3 zKXE#Ht2>Tecy_aYV`A%DT!Z9jKOquDJ|rdl=Hjq3cj9@^gI9zquZop0+&K{P{N|)?_#?8nl_=W~SOH(U zYG!YIqe<4SUZb)Wi)&RERnQ-8k+LiL5swXDl0@>h{NxQM`2Jx6hhJQ`c?7Iv ziSBI{tQJ@%tg;47nEe)5ThZI1@sp#Ug5}S~P<)hpgs@4;HDusH8_2d645Nw?bE4y!Rftg;}in8$@D_n5Z1UaW?R zZJ6Es3SF&DPFR|^^+8yd$SN#rZkASB>E1M0!D+l8j!Lj(SH=)l%(g-5Xbiy8>zb{U zU7E*xuyk+B-$q4Pa+75$`|I(3S6HlkWMMCEhqd%JZ`j2P*(0!YZ_D3CMp$x_l_In9 z=7V6B&of}LsT_vEM$I09C3#zfMJj|PHdzH$_KRiNK<_CBrI5 z+YpEyAi5SfNxvrFR}I_ETn}&ngJ3DCfbV z)^z=#`ZR9~Df>qRmf~$WSb3_$Vxz*UVAm+HH=aNy!kW=%RSp*FS+MMn8=Ozae}Ps{ z5VP+mVX5AhgN3yWi%nLj4{)RGMIx{+EzLBnhOl5}!7BJVf^Rc1`(YB6=54ivRmrf} zWQ~2!1-YdZphZ}5XOiW?D#{tK8Z6zkp|I13T+aS`Hw8=cw%!Q~sRFAJt>!0g#c#OH zOjyFY4pxhL2CO6UAkY#@_Tv<+7scDM$!RqOu=s&w?>}vhI=snmlRLut(=l`$RzkLv zt+edzBrL_-@?iDI16XXb=u6aeMHj*N=yX?Dm(bbmuuk2GR#rjHzMX=lcw0kqsz@ne zjhZapaiS>g%4GavKiqCi+JwJZBg;`dt3 z+ZvK{wU%q?R@8PwUr7|HWZjC7;RR(amZ5gVXX8RKkBiD!Bzxd$@tFc|OIiEJ+xn@$ z?7@Km00000kpF8VsH&=}s;a6AyXN!SN)^TN2{?E3-bwC^i9?+q&J1Eq=os=tLPj$) zNyH>3@x8>5_F;vTqM$wz6`sOVdx{xEjZv0<4(G^~~Lp%o)6ZZD~(VM&@bF~WZ@Q95(WEh4`<0v699h`@z8 z%9o~-$}kk-6mlgqbexuk4I-Qpy?*njx8MBYv#%e~AE+YbIYcrHbF^8UWOm10(~C3z zB9}Vdt*w01r-YmmTX_9FAAJ1Q`I|2N`Z_v10z#iQYU z#cH|dYlfhniZuVxdlx_X;;H*zd-}!0x7mfr@;n+uh4w^OGL!1S6k~_AIj+|$wSnW) zQ<1ZT>u-GWwNGBY_{95P9bxwg5nl%PeC*=W&mPu#_GzCWB$~}aHJ6q%xpLDtX{<&I_J=x!VnbH!N@;jDYGGpi z_nH3s_uhQum8TxNe2ajVH`@ejnr1qKVY_4NN~6uspoL?7c$N6Q(nOJ!bbVX{TAD@v zb6DupTQ9!$$zzw#ES}Y(87wmlok6*nYv{?O-r06ArG%M{IHl5XYcG>brS$B$mXBJH z=Fz|O&MezrMK6VfXRkbe>4V2#JphYP3B=x(;Gmh$pDj&yGfLvNlwH{}`xp~1)&hl~ z0Q%kbM)J0VoXL($g~&&qCkTg!7Xhgt1V5MAaMBkgvEP@_)miwjEu&8!(LQAN1LP*v(1>5QC6q?Gi|wcWsQQ9ssd zju6B&F{4^1v&LyPa@Eaz(?k-+hOw-&5DRNq!louk%UB@RMEOCW!JAygAQp7xx$~dS zVW}eAjBQI_65-bVs9PKFWE*K^jX$*wTO1lxQUxjraSGzr(wxm)I+*~;=|-o*3@Qc= z8tyNFg-t9htJf^r(a0UmV$z~@MUM9pTBtdowdZl_MIlFgiJv}KF zLJ`uawPc~hqLS4*TPi`{>MrJqJTW-i9+!HWs2(?YdLRPlO~Zr`>&}PX`x0PrkP@ZD zL(~kL`5IV-jPg6FJCaE`vs3E@hU&TqQ7vYR2wRFo?36p{_4!sQb`J6-W@U^V;`_^$ zX)W|T7cFjbHF!a%*gZ~UA)b~bKq7%5h6T^B9Q)EPgNx|gs3X!Cbp7F z{H~tmI{MW3okx~$n~(ZUEiISN2cp7fQ672)pl%qx_KN}%w-zB zA4J`)%?ripv{)VqXD)qx`B@fO2GrEnsCTy39#6ArMN#CnKUlFQtt3CHa@w%I1pD{P@e3=fcU|NoR{&&f?7^LB41!kWZQ#IU1IPEYoTBc<9*n zWV|^dZ$JI`tJky0(gG*ya#Lm7=TZq-k)hd0&GuADPoWlh5~lM3^Z}C`3#{#_tut72 zTyi=Kt)fGhj3_CCrKfYHfhjB!ZkDG2t6t5w$dOGE#n>EmhovG59o^2|R~XSppL_hl zGZ6D|Hls~IW$SZ9DH*~m$W?OSolWqYG9T~u;2u$Dl377Hk7$@ zik#5v)gibu2I~sJxCLi(oXzl1+Ao0BsFa&n^-gxW3oA68?T(&ek8o}V>yK6uhBkyZ zgVugnes{bJNdwl#+%u;oirm~2k9mC9{LOxESj}biwN&z8TtVPZpy|h6yajN%ElSG=9%!ReDGfs3=u57;uiX*x0AK+S zJ6GzN0?=XykblIDh~^ab(rarnz}j{+vaGNmm0;iL6{?*^hTj@SYSn?+$A<9so#&tX zzPDY;+K{XkX-#Pq3(>6KI$;r_HOrO8dg5b07C)XCTg5z%haU`BN zE<(*_dCyKR+(SO&3QW_)^Up9P4ZGvk{rxG)g+u_17j)5V$ z11revaG2NNHoCr1=Rr#0Mt8sW!b{)1BSUOLc0UN~oXrZ0&+IHz)RU*hJoko@T9h4Z zHsi`SoiLzi-px^sN7k%p?vh;X^-g4%QDbp@4@sFxU6^n2g$eV^y~KJ!R}nRpJ$h%JK75vL>vrzRqn zm_|b)2Z;VT7%cp~hnJ>7{+N?6$iiX3$^x)Z8K!zW_-nnr5+own>~M3(q$6yRrks$4 zD}Kr2`T3TWa^ayZ1J*K9>IpPB@26ebI(ou0hNW;2^ROjao}9c8kp$4fQU!&?vx0@u z5~X>emWM`V9pl-sV*<2S!UC|9{2-u2=0}ulYTl!aKA309PX4Sh=D!Idj3tA*T#%pK4I1{Khf@(^Q1w|@-8i&BHAskH!>we$o3 zhmAQv(h)q|MTO*fGPir1VxP$#{j!+P*dhmg{Bi4L^<-)YJqvW+OM<1z@5I^Y4cMJyGFC_*jU+cVvzv4{ca6AR; z;R?Vrd~SzjMJ8bFkK<{J6$n~y2Noz4vRK$w_gqY+@p#gmxeQi(Z46GXo!1td?X%uj zdYE$xWQD1z?(T9vzetW_aIlWA03q;pX&T6qdEh;>Gz{iUhu`6~w*^?sy2T|<@#d1{ z10>ddq#xF@cBUU#NDZX#j1}hU?6o{$F~G#;a8{;oEw$ii|KY*4qH?~xB)e1u3)W@} zL`0B>2Xm?zqP)CVxEU;Q&%(`H0-=4v()X_&r9tep_PpK5UZ4i<1}qP{r+{g@(gX^n zQhe#rcv0YQja|+w|g zaAcA_@B7EYJwrORs((WFwE6Y3XD?sAeD?B{R#m?+)AGQN zKLw>J%5IY67hQXBWBKlo{(8@H`vYaSZO-v8-PE6=}wD;&2RKNW3^Q*sCe+|yh&o3=44TG=wGKpmM^)E*|?oRr#*AtP+ zzyQ~M{r!`<`rL{OWt~Y)73#*B8*={C23VZQ>$;XAp&eKkOF;Oh>^WH8z$Qh;CHO{C zsoU$f*eWB93rzNT3$Ug*ONNJEbd0`wRw$FnO0%=S3q?`;?z``bGU27b)jvM^;_jrp zoVmUccnA--*T8*munaLRb#%6f&hF;fh4;F}%psCWx`v6v~Z&sQl zD=v`>7T2I{kA@Pg#8goXNLah9Tg9A5Jzw>{82LLZUIIsHN+TjfnZfWwl$~9gFA4*& zFj|P564Dg{fkJRd7=^I5Y|?6z5cNhdQ}|!qQToa?TzO?`hY+m53ZpZrjEIG`FfEO z@-KZeKXl6uEU501M8vo5-NG{K=AiLKrk3V}heTw9+yEk;)N*E)wzXq1Em`d8Y8K7u zJI#qg`pV;PZVt^Y^f$j;g&LJvD?$Lt@P zAm~yZj~DR(toTrHmNB9Z?FbeQ&x@D7|6zFMvwGFf&*H)DC@KsDxC(=V0Wt_c&i9`7 z{@s=q!(DR>&Cly8Vnwc4{2>f;3`RihtXXez@o8|{CtOVh9p)}0F` zI7#8Iowot&uHF_bhTQBfB<(}s(!gRR7AD7d(!s&Q#&DU>+T)Jc*5L%d1m9j^&Reuv zOgy1L=QKQ2t=9a7!Ga}a)Qmn|=;*J|YA=`$fA~&kdh3yaD>zP_)^?p=2a8NTl3qFB z0M^j(rE=jqhvNav3>+Cf7PRMR!88a92pu9~fnc784s$R#7ySZmM?f-qvGCyrK*-kW zN#n((y(?y7B4zi831H^Aad%E#)%^O`)z?4&^3>noziL#|^g(B5<<;@ifQL1%YPx#g zhcv`iG~{cuOT#Jtts(vJN;iO|!2W3@6vpCnFMAtS!jH^Kgu5buszl}<1>48(HcG}w zaLrOS*DmkGgv3puyz5S9;KYPnwh`B|fJOs$^K9yWDWUfFb`5)FSa1DY-<@C1NG zG0&tVa?`}(dV!A;6fJ>;=3?>s%A%VUHUh?5(tfEF-Z1jS7h0^YV7X$);ADhO6>J`M zUPCwmA_=)jM?}h7MKOOc{CDk(g8B}%w&_mg_|;PzZ;tm}f`xo~fYvEvjmt^YI#A3d zm4;h~OBgJMz_YF$Tp=6Op8&hvQSXl3QTr0!EEZVpWO!}CDgY_o)2{^Jl7JMBGSQYN zl5$hC!C(5e;UTCC_^t0`SI4NS?aD^IL=WjLCF5|z6_P}hsn7N1x^FOTBy#lUpBPf= zx{=W4iNIQX#J7OOiGhBU1q2iJ{gU=2@aacZ9Q0d=hx6Dt%@m4i7q+Q)vCiBH*m!ke=e8|aL~LMo3|+WeO3PJ%qb{I*uBPe@ z^q4rTm*hH}i63aHcX7F_?KJ1j75t5 zR#vHTshJ2ZhUGy~BT*ZizzuQ(yV?4{NC`rRx;%Hw|0#;<#M9H$E8qTkz1}~GuWCrf8&}MAQCDN zSU{b6xYA*mg00Z^fIKq*7TjWn))w7L6#-f~$s#12z|x}Oha|zN-k9C7G<5#Tn!a)2 zzV7}PT{T9+UZBL~D&t;LbJdH2<44uixm8vE6(eIKcib}4FQ{tHcNNrJR%<#Nr0};s zRzder?oq6qyt&W={1+3zg5%<)sc~TKJ&2IPp2*_@TDEITz!8hZaOVbG2qlHG2Ky$< zn+MKqC36?Wy}Rzw@@W$fYlNdf7nY!uha9La=u^&lD!Lj+)6BcZCI1_D; zqanYj2nZZ+pbDwT)Ci86Gp;));*|2+Cv&Ri6IZ->SMGhR>d;gYw)g52NA4K=PZp?6 zmF^=SHjvyhD6~_j8k&z6j8x)3Z5_V~dFw`?);Oa~t-rx8E>=FD(GRB-Va^`k^t9*a zEh*Lu?kVHm=NH*;HpnV91(DvPY4+Lch14#hbq%J*}TcUndmUW z)pHx~d+H7d7WMrjO_j8P3z~6KW#@@>DDq$`euE}wVVh!+4c zpk$W{M@Fg*5568;8|rzCM;|7(uId2O{^h7Kx@~~A#TCIU3d7lbjtUi4UwT@nLKW7C)wa@o8B`Wm9y6H^`E(WZP6@Z2(newD#zi>b}{5 zjP#_k_i}4o9HR!+cqSAB=7!c>X( zMLSi3%Vh-tT5u3zvt<;%0)Zoki=nbh;W{C)U|X>#C&^|cYF zPMAhJ&gVwAl8J^3xkpdBY!&A|Y9j0j9B`<}*VthmrXy3*PFQE#uy*3nzh>TuxdME}D-fBacDEC%l& z(+7#r7S0cz;H{a1^(lgf-3t(osGF~$IOVQUpMU!1?^nOn)Z;r{C=k|95rGA2VrhVr z?tmcxvl{$lJ8iWXOzgpaWKjQm1tk&i0r0&Xn^kGtjib$Fr;p5C@GKAUYNOuAj%p%yLmjb)t9rUIwT!8L1_ct5s)w3C-2Gdyo z+0UK8WZe)q1wKa(U?Ep3Lk_kX^sr8{%b7ew5KQnlz5VHw zag*wNZ^5~3!Rl4DIo#J>*0=cEFS-`6(0F*phT=muaBP_|H!cNRN? z6;=KpZC3&mWf_KxVpq8qp@0NZKtL9=fWk!%QG}5D6at#aI0Uwpr2%vU7LhnsE)ft# zG9VPp0L2PnCQ(5%FSIfnGw&JA8Z4`+X`1QzzJHgWV|QJkUT1WiS(bmF{l4$Lp7;5- z79KZ!E*S60Fg!%9QP!pfjeCohRcbxfBt4V;9HNXXw^}KMhKy!-5wOfLoMCRROE;n- zjbi0ql$=6%G0~1A1M3!U3l~BX$QeRYE-z(=F4SQm4oR=*87!`~H43hf%9|Pp zpTs+NJB1BZ>9j#0e?vS0h28=!Lc|Yd(+3O85eBVG7luWz`1l$b4OK`*qKv4x>jfcC z*w|}nVAfAF0W53W(pFJR#_=B#$dTpe@5x;fsKaAwq!|4n&4MLFr45@*1z3vx$1|}o z!RA{Tv@|9Wl=4^YP_|24F+R;Sl8VJ`bKR4ks~1Z$uiSS!GY1xN#`(^T+d@fRuwoJ8 zcf%b*#2F<9bf1~R=}hhQ)Q_9DlU<0xMld0mYe}pqyUdCh+?vu$5ysv41}UP2UX0AtI*$ctmNI$@y5oFFeU_}v zUL+GXZO-`Z{wP)zgH~eb)!Xl67Bwnl)65~@ti#kjEP@1)(Z+@|1`7nDgk$9=Mux@W zhPIrZiWXE~8iNax7ij`koj3`LX~N-CTPBo|bou}(&R8Hr<-zI0yQ412+~;QD<_3v^ zK(Z^96`;uW!$IW+=`sh0cmdVD12KR8+9OUVB90Fo8l}R{=cJwOO`qQT+945GF@~KO zfAxxWl>0v{RYnI7L3NcEh6-Frdu-+wyAw3Y6Ya7ho5m+9GLn0q@RjaM)6&Rl%D9sQ zs48Ji--VA84Ci}8P!O#XWKmE;A{tvJ1SijhD_5@cvVg-^TH1t}K&-)>3-){=-Mv&WUsnvS{5%wl;OQRro>}hx!yNP`8+7;tb6U z0W3fF!@?2KARZ;Dp}~h!(EzM8vaB354<~|^WLnpLp*jKTNhr>6dE_P%^3|HVc|pw% z<^T&=sQk(pT8mq?BFI0U?!L98|IR3$BoeL_T_0>!hFj5){2*xQ=$SJS5p-R#X!+mU z{7%#$&4k#93}PW}we79_yV{6L9L%_$T$OWOOGB&6+sixt=361b#>ajxdNs*x&`O3TLDgy%%_6_C>DJv;dN1O6D>+ji?)C zo_r*j0v0{xV;(kTCRc_nO0hw#$ef(YSVy* zwSpS0d=MQlr3ezN7bI_E5}jU(bn%Gx@QC)tVNH5bOMw-=F^H(!R$n(zaaWA23|-hM%kJOK)I!@fkgsqoJNc&725JaEK5bf+Ohn}3=xEFXdceqhjREsyGXyJ%fJM=&EtSeD zH2|v;(HUFVL={f_)zJacYE?4zPgS}QhI6T|KMCzwDeo~IMFcW1RL1RG5}`zhkvlS~ zuZ$C3J6Ce$MySAbj%u`=MsLg9ZRyMaqpuE6OWq%;CXsa(gs!jvBBM9pQk-yD`?l!nmo2PX@z1P}<~qIABG6n;(o=qKV{L(uDPySwOR)4_gG5?OzDF94#tS0tAV58P z<5w5vTek#LzBY3ecX8R{(W)E+3n-Iqc&+zb%8%t>wM|}ERQ|$(hwcN9WF1I<7^oa6 zp;>P{Zv)5G9Dgqh-6dRgOhFF3w(#)Hc;tnu9pdOMq5ZSAi0O0*T2M*BIK^y{f)k3( zt?qhfA8yJ)k-(DqD)*K>=G;oF%*+^~-x~uK3isE}ZA$sB1SJ}_$VfUPjHHX3B|g77 z4Rqj`j(B))RqP6bG5t7wXnawLV+0j=5_S9rZJXqi_rsj#s4>B#uE5z4oubrpfdJEJbNB_WA)4#+qwBAH)8~0Ipch&Di>$yoi>7|Lu*N-oEw;J0NXf zAeX=Jchk0l95__np`@r*0eCzf$F^tpE%4hqC^28v$1Tr{~vh8;=!K{=85D zMX^fYrx3JRK2KtO&t%kh8vF=N12%vf<4y09Mx@>-c74-VN%J@hev? z4Dk1NMp$T!tcFe~=uc5&r%n(}4vk1k)Mf9h2E3=`;RzA#2!KzRLR-9=;l6yK;ItRdbu}?j~dhyLoy}uW}kzgkGhG;fK zItQ&lsO}oMZtZ*nGcyI}XQE0~bR?tmkAA=Aya;flg};3rnmFfiV2xe2fErDBsDQFe zXJ@Y`z#%sLP19+)cP>p%Q{BFw`)pEGI`h#uP)XP;z1F+w%@<#MbHlY?jwR^J&qC-? zy2ivW6(9km`rcEsS2IfIj|3%l`bssnPs>g|bO2frD#5}9Z@RiXe4R*0Ni@$x+XC7; zv-OFXn=i?V17lec4UpVEOHG-y)`ogR><9$1whsta9}uuoe*DHRRpDp7T_ObND3b)x zjLoZAs33zwM<^X|0mSsR419BHhVP+ls1mGO;`OCyh48ekXtgvD?IXAun)%EQEd8D5 zw^T|BYVqmEz^YWS2j90(+WYJp%nxwrZ~-1$+TXhoSUm->er>K+OtLygNBS{PAsSRx z%XLJlInpUUh|aold7SCV8G$5K({>|FploSHMBsqGEs+)jEORdNJy_CS(?V92b|z*( zXHXBG-TqG8dW`WZz)DZnQf`ZKN{ih)_u;kLOr?T@6YJdP6f6YszkIBibTaf_Om3OM zjTri>RE!muU@w`rVwZebp$7G=)r9Z%orr1X<~k7I>f zD$}BT*nn_I6^Fp#tpNpMcdDPr*L;JmsSw6+yh2A!uT;sxu7Q7IY3k zlV>K1Z4kpJ9^3lL%|xdO-NOi&6C`48-jx;@;rpEOgBIb8lJ@jVG;rjGj;RSBmY*8w z%kP#Hg(z}WZfVAKd;6(<#H?j&YHeG(JBxg3ITS4Ly6ZbO;B*IAFMhEh<@Yah4VTNk zo`D*ua81{`Y=>|QkRfL%p^5Ut(~I7_d?!@k1c)eLXU?1v2%H3AckW!hm4_l7HsEB6 zA_r8!7@&nXX4+U8r1BFk?LV|DzvI;lQUKWXc zIE)Ym;_bb2`*FNKMZx+2K|JBJJ-!P!A8GR(|5fyEjTwSP&WenhD-VL=myY?w2&XUz zQ(R4Q&p9cpqIu+`>1D79aNir&7D%_68tacB@dPqWUsv;4m-4=-f<`%59GiMsm95S5 zreaClc%pJ}?GrZjl2+tK6f9s?A?koD1XOac4qZ|U7JL6eSimX_o6j69tm=@ShMI=o z?%#w$Q&`NI?9tIje>}W<<8G^c^5)&$Z54!%L0YVY`o+`5-)Js@q2h^4yKte5-ZZQ5 zjtK{=%Re0!9>0A4)q@95cL_6s6_w&WRT=Bc%5t8r=y`tMvM5YfEHlLjoiFM)IW=OG zY`N4(Ip^n;jo5?~EE2?T2kq$ba;Yw^q{+{g{N*hthd(VpcF;JM zf9X)Zq`;Qu6G&k&6xMLEtspp9+S%IDk^?Jxx!9<qJh-wRhLUs<2O z-M{MxOr)qaD#VKl1CO$iIJ3}l09qB&*4FygJXq_M;o;%@%(%F$-QVq4{o0NJMM)Dk zh~u~-@Dk<#u;MOpBg$+B(sY>TVf#GP%O zGNBm6z?Pi#898f;){8QQQeR-jp%E!z8Xev61;vUv;X8JGOC-0{7*h`f0hvel@obDk znmwgH1#60miq@epWTXh2p3`ic(53mJYdWvYVyWTJzHrC_n9 zV8Iz3;P3SK+4 zr>Hk@xV||@u?xq*dU0cK|1W5@K4x8o(TkULOn8B_!%Ds|LmKxGtF>)xw8Erq&F&pQ zYxQg2b&IQvplD=CXT;1++}teOKz)n1=EK6K?VnV?m1-D^Tzok) z#M9feOnb&i3}~^`8VyRL7E!w{6Sd{Fv=mj4J46*uv$jB#LR}#>{7W;Q1>5K@S0_tv zZncEos5o1&rV4t7_=pC87Qot(pDA*0BxHJXH&9g|+ZN=%G*J-o{rBIyEcVbe>v1&R zx$boH;giFMo6mYAq&kucumR%i*n@uzW2SYi#l;Lh4JN_d-IFS8hJ1^QjY7th#_%%V z`rW;sQ>a+5el5Jj*P}&vH5VSBfl50hv)16I!CoLy31&nvbWw_vWl^+Nf7hLbT8ar+ zV&GneqA#1B{nC*)JhpE2-b%(QH1URJ5C}O|eyZ=}@se+%cP5~BNAeLf^bQ`ampRs~D#SRkCWCdTI>C?zrONi82f0eal7>n1;G1SW$x7WK zPkyHm5Or2m7~)zB7C_P0(UI^~G-4@}D0B;=&`+w@ z@*T%3YZ@ll?H{R(>cNsy2s^;WajyXsg^-qZ@&BP5Ne5Es@nCK0z4i;3rK-;>!lj|E z|393OrtDEFj}yprrzDw8J8;MYt(w-avj*a$y)0NXJp@`LJyhcdE&f;a5DphNfez)p z-wm)RTJ5(UMXuZW%zT=unoTRT;uB5QruLBwpLr*w;;fo<)kQ3n=-MSML@qKUgv*(f zU9eIj1gU4;e-a%+3o0gb-;~gS(OTJT7Nqz|1%wCO5S*dbTV>l#(LP z3Ry$O)6rCD7yE(=tQ-~+J(&N^NV~-|2n5ZCBP?2d&Bt~!&SnNtXjk+85BaIFhUiL7 zZC>yXUL4V?TlQRV$h^r%1M9u45Z3qicbwaRVls=BB0cOsL`0&d7`=Pa3(Up;5v)KS zz;bXs5_#-U(->L_oqfBt+lrT5(DZ%$ZOYlT#l z;&S9{$rxG#SzG>CoNz!h@(h{T$j+t8RD70j3aH z@3p+5ig#_P^BcFuxQ#YuZ^Pwd|b?wBTaAT`16%k6zlTS~t1*qN%Y zk0?|PuFo7S@7vwGX;W{P)SaA~43gA=ZU^~6D4 zNtv0?ET90n52JakQYlZD&_D>YUf_5Ba1B8VdC%^w_J2vc){v&cFkGkm+Df|ES}R9| zt(j$6VYN0Ba;8O@XjZGNNGTSIVOlUqWWkasDUj*>mvl45<<`xL47w3kNP$uP>>?B; zR0RF#dC$(-x4E z?bj7x66!>lR^jJ>EGcNtvk*KsqkN#UqaWiC)~e`n>aE1aR7ZLVs#Pfnu{(V|%{P6& z3at&p8ntbR0uGiQd$I)B8xCxbWGS{hB(+kNialAzJq1;e<41D7OH1W;S_S`%QLpetez}{xSs17xmI4$g7t^lWxow$%k}WxAvss6-#=NnhHXzb) zXv3)rn?C~K?r3^#1WjM54kZY)mmr$N)Zci~WAn#Cp+r6$Afx{351-wVgFyC>PR6M1 zQ0LHFf}k%*OUowkA4_@r0ShRLPOIe!|6$e%^m*bhr9?jfP1Q4d<9zE46_A zBco(zPL3JFiY6e%Z1m@+xI&1r@O?H4v9lItCu@`m1RS(JdEcG_R0nL^iP64R3l`y~ z38&oseBiX-Z9n(C(=dsp#e-AinAi(>FZA4p3$E%E$+0TkXtu1=I{+phj3o!;JPbY~ z$wc-xOrV;J5UADEE{SSZlqkpmG)b92eSI>>{o*a>>U3+hs}iaGH3nm-vHW>0-Qt4G z1R<%5!zeZM3|sc3Ws@enS*Msa@91$caO!S;^W0S?%N72>DIADqkEJ?= zMk14%QMaM^`NO$dTP?iMYN7zeXslH0(;xlxLX=}Jwmg)NRB~}tG_Z=&tEeZ1meomz z$frys_M=fkYKBCgl8Ta`h`93mqe{0B3aspL7Ftor!CRMT_<|Q&*Xs8|H3=!P8>{Ss zbC8&iqhx?(4`n0B6-)q$63jpy>Yl>1AZ8P{S&_diy-->%jAq6JD|NpSg7UD|OK<0X zjo(=KF^UB*^mxw;@|!g=Ttvwf0({^Gr3Z&}b#7j-?iu z|I={6Qwz3s?8aitLj#>x7AB==f{V8BiqmK0hpMEHCBZ13sHX61Myeu{u??A5+FQ@| zZx@2{zNc1Fk5fY!3tnhT-EmtxywLr{u$4SO6!6(&dENtC9ra35v*VO0n2BvGz>i zp!x-a>($BQcLZy7w03#kytFkx^d=ja!ycEh&a@0(Dwks{kz8tRlQC(57)wn}k}{3A zva8Sy56eTuZ4_gmF%>aa7|>pe0OE19xt{ZZQyPfP6%}?nd7&=Wsy!|!4avO&@Jr>* zTEv%h4uf3EQV2{t6iO@>e&`|2Wy)Hmpb}V;gHS|_ERS1+1@-?_e+`|z?R=OS#+rNZ zy!~Xa#bsbi*7%^ZKKSuI9%Hdiw$fnQ(99)5;0uk?(;=;rYhI^A^&xaoNc)aF1VzML zA(Y>=x%6m7k7;&#>nb`1w>N%0J9o!Uem#WJ;PyFPw^D>RatKkkO zrzwuUVoc|Vu3(m;#UN?_<&6{B%*39JP{ea!Xl111+ekw9Zin!Fceh`a;f(!!=#F>0 zDL@pcYX9cohQc9iI{o$|+kfmlC^v zQ!-;YxF~qb_K=4f*eb}7$NEAn-A;jryTNDs#EFnRYek?`c9YOXh>XPuPm-9+O&PLf z2KSLa?dUav4BBW28REB^bY#e`>qpPv4t`SxQu7=acJ!Guh3A($jYq4STOe$g+Lhz5 zb1>GhP59UQqiQ&AUf};rT;NFY0;kO9KH(OY2lgEuri;aKbHoF0@E4$>2VWkQo`A-V z;TVIl-VQnba{N}m=~}|?&_QpR$i|v4{AC3$AJ;D&b$V1|F|42eueWmxogs?jIKLIS z-TsACWl~P)gWQ&sUpeP|Cr0f#%Ko+$T>+(Rk6%PvW zO4&icIHnJ=j1wsvA~q39MGE zm=r#+RjYwcvK5t;EhcicP!QAvJrU&)pI?tNR%a$0Ts%mF*v`hX@+GGq$yasV@=N} zbD&TIxgmOCp)gTj&Sy3bYh&RZC6uq0%fz22s$s=cZC7>*gq3~wlc`Pyx7a{V`eZ$D zqzabF^}C_mNE)FSc;kl0KxP*PYozK7pm(zyd8IuFL9rVTroomWFG%|H=Lu6LSZePE##Mk4SecOy)d#K24>ay`hv zfzdEnIz=nh9q@Nx#p!bmSd(?ym%D_2YxU|71Pj5H<~Wq5yN1`Nvm9>HwjB~dDdr&dtpgk(w}lC&z&BGcS3?-B~@2t%9BqxED1Eh3X+DX z5%}B?1M%@zuYrbE8`g0$E4N+KqKP<*9+AN;l6W5Fx*7t@WJizFwL_oMIjlGxdbWp8 zoMrWTZ}&NuY2752_sjZ8#aU4X=)eLVgYl%$rHg?$DtZkxoMAUO&Wcf6oV1H*+XyUT zU6CiaZ2ogrER)etAE>&lGtP?Bp&k83LpYI)rz$%29XJmjmC?|2cUcj}!@iUf1525$ zC9i=7^9vpg<0d6ICT$2-TT7eYe;{4e3yP})fu*W%Qrcc|`sC)}xqbw)@Xb7z$3d2V zl(GJrz>0NK-FeXqD^trrgJar|&|xI$tstFZ(;I?iZ)T2lH7CjSu+dWm%UKsJq&RsL zwASCj@)|b*5ol$d@;J!yg#`&Lq&Qjf!itn?7-$g3zabI4&LvK45EkA?z`B|@J|hGQ zO}b$vRM>NHl1*#R?4gQ)C~Emp(y2QKS%Br6Rba`3a-33LSg9HY8nl58WLTcu&BLBM543VS_lypL_1XEJImoIF>lF1>XAfTYz>1W@4K(QKd@}wE ztZ5UTui`I8!W%e*3sw^MQnBVUi`M#`4}xH|EQ{p`vi<~CnhbTpDKD(86l$PBw_c;) z`=_vY8nJi(=kqD%x`C_-Z7>>fC$g^R+}U$5xzYdd6(O zzMi}3d~sb5(i*We1q;7mF!sYqQpzGSg7WUkld=Rzm9^iWascens?U5S2VPv1S0zrM zo4PKuZkdQ2A?{*jO73a2ANv6-FL&wC@9`>Cu^4$ZOC?&=A+2pinwm4iqWNjognmA0 zE8miZ6MTjbZtpy@zVfR6vye_zHfG2lqy;i>#b-(wq(S;$nh+(-GXJ88qd%N?_?VH$6h2?DBgFjBISKqB| z;9FxtfAP5P!#dVbhlTquUd{1})3paq&l%dKW8GT0_wvUT&A@tzO=-@GQN1i1dJP&e ze8`wacU5WQR%^UK40QP8G_$gx;uo*_&v-F-P`~cAv}Ni2D;qX^`D*FYww-%MT$t0n zZf%+R?FW`RY-&(TGG8CwUG{sS*l&6Ye!9FdR=)r{&^!=#IK=D#0000assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216)); + $this->assertEquals('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0)); + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testCreateSecretThrowsOnInsecureRNGProvider() { + $rng = new TestRNGProvider(); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); + $tfa->createSecret(); + } + + public function testCreateSecretOverrideSecureDoesNotThrowOnInsecureRNG() { + $rng = new TestRNGProvider(); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); + $this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret(80, false)); + } + + public function testCreateSecretDoesNotThrowOnSecureRNGProvider() { + $rng = new TestRNGProvider(true); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); + $this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret()); + } + + public function testCreateSecretGeneratesDesiredAmountOfEntropy() { + $rng = new TestRNGProvider(true); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); + $this->assertEquals('A', $tfa->createSecret(5)); + $this->assertEquals('AB', $tfa->createSecret(6)); + $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $tfa->createSecret(128)); + $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(160)); + $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(320)); + $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567A', $tfa->createSecret(321)); + } + + public function testEnsureCorrectTimeDoesNotThrowForCorrectTime() { + $tpr1 = new TestTimeProvider(123); + $tpr2 = new TestTimeProvider(128); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1); + $tfa->ensureCorrectTime(array($tpr2)); // 128 - 123 = 5 => within default leniency + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testEnsureCorrectTimeThrowsOnIncorrectTime() { + $tpr1 = new TestTimeProvider(123); + $tpr2 = new TestTimeProvider(124); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1); + $tfa->ensureCorrectTime(array($tpr2), 0); // We force a leniency of 0, 124-123 = 1 so this should throw + } + + + public function testEnsureDefaultTimeProviderReturnsCorrectTime() { + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1'); + $tfa->ensureCorrectTime(array(new TestTimeProvider(time())), 1); // Use a leniency of 1, should the time change between both time() calls + } + + public function testEnsureAllTimeProvidersReturnCorrectTime() { + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1'); + $tfa->ensureCorrectTime(array( + new RobThree\Auth\Providers\Time\ConvertUnixTimeDotComTimeProvider(), + new RobThree\Auth\Providers\Time\HttpTimeProvider(), // Uses google.com by default + new RobThree\Auth\Providers\Time\HttpTimeProvider('https://github.com'), + new RobThree\Auth\Providers\Time\HttpTimeProvider('https://yahoo.com'), + )); + } + + public function testVerifyCodeWorksCorrectly() { + + $tfa = new TwoFactorAuth('Test', 6, 30); + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190)); + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy + $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy + $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy + + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 0)); //Test discrepancy + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 35)); //Test discrepancy + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 35)); //Test discrepancy + + $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy + $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy + + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy + } + + public function testTotpUriIsCorrect() { + $qr = new TestQrProvider(); + + $tfa = new TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr); + $data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE')); + $this->assertEquals('test/test', $data['mimetype']); + $this->assertEquals('base64', $data['encoding']); + $this->assertEquals('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=Test%26Issuer&period=30&algorithm=SHA1&digits=6@200', $data['data']); + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize() { + $qr = new TestQrProvider(); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', $qr); + $tfa->getQRCodeImageAsDataUri('Test', 'VMR466AB62ZBOKHE', 0); + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testGetCodeThrowsOnInvalidBase32String1() { + $tfa = new TwoFactorAuth('Test'); + $tfa->getCode('FOO1BAR8BAZ9'); //1, 8 & 9 are invalid chars + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testGetCodeThrowsOnInvalidBase32String2() { + $tfa = new TwoFactorAuth('Test'); + $tfa->getCode('mzxw6==='); //Lowercase + } + + public function testKnownBase32DecodeTestVectors() { + // We usually don't test internals (e.g. privates) but since we rely heavily on base32 decoding and don't want + // to expose this method nor do we want to give people the possibility of implementing / providing their own base32 + // decoding/decoder (as we do with Rng/QR providers for example) we simply test the private base32Decode() method + // with some known testvectors **only** to ensure base32 decoding works correctly following RFC's so there won't + // be any bugs hiding in there. We **could** 'fool' ourselves by calling the public getCode() method (which uses + // base32decode internally) and then make sure getCode's output (in digits) equals expected output since that would + // mean the base32Decode() works as expected but that **could** hide some subtle bug(s) in decoding the base32 string. + + // "In general, you don't want to break any encapsulation for the sake of testing (or as Mom used to say, "don't + // expose your privates!"). Most of the time, you should be able to test a class by exercising its public methods." + // Dave Thomas and Andy Hunt -- "Pragmatic Unit Testing + $tfa = new TwoFactorAuth('Test'); + + $method = new ReflectionMethod('RobThree\Auth\TwoFactorAuth', 'base32Decode'); + $method->setAccessible(true); + + // Test vectors from: https://tools.ietf.org/html/rfc4648#page-12 + $this->assertEquals('', $method->invoke($tfa, '')); + $this->assertEquals('f', $method->invoke($tfa, 'MY======')); + $this->assertEquals('fo', $method->invoke($tfa, 'MZXQ====')); + $this->assertEquals('foo', $method->invoke($tfa, 'MZXW6===')); + $this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ=')); + $this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB')); + $this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI======')); + } + + public function testKnownBase32DecodeUnpaddedTestVectors() { + // See testKnownBase32DecodeTestVectors() for the rationale behind testing the private base32Decode() method. + // This test ensures that strings without the padding-char ('=') are also decoded correctly. + // https://tools.ietf.org/html/rfc4648#page-4: + // "In some circumstances, the use of padding ("=") in base-encoded data is not required or used." + $tfa = new TwoFactorAuth('Test'); + + $method = new ReflectionMethod('RobThree\Auth\TwoFactorAuth', 'base32Decode'); + $method->setAccessible(true); + + // Test vectors from: https://tools.ietf.org/html/rfc4648#page-12 + $this->assertEquals('', $method->invoke($tfa, '')); + $this->assertEquals('f', $method->invoke($tfa, 'MY')); + $this->assertEquals('fo', $method->invoke($tfa, 'MZXQ')); + $this->assertEquals('foo', $method->invoke($tfa, 'MZXW6')); + $this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ')); + $this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB')); + $this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI')); + } + + + public function testKnownTestVectors_sha1() { + //Known test vectors for SHA1: https://tools.ietf.org/html/rfc6238#page-15 + $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'; //== base32encode('12345678901234567890') + $tfa = new TwoFactorAuth('Test', 8, 30, 'sha1'); + $this->assertEquals('94287082', $tfa->getCode($secret, 59)); + $this->assertEquals('07081804', $tfa->getCode($secret, 1111111109)); + $this->assertEquals('14050471', $tfa->getCode($secret, 1111111111)); + $this->assertEquals('89005924', $tfa->getCode($secret, 1234567890)); + $this->assertEquals('69279037', $tfa->getCode($secret, 2000000000)); + $this->assertEquals('65353130', $tfa->getCode($secret, 20000000000)); + } + + public function testKnownTestVectors_sha256() { + //Known test vectors for SHA256: https://tools.ietf.org/html/rfc6238#page-15 + $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA'; //== base32encode('12345678901234567890123456789012') + $tfa = new TwoFactorAuth('Test', 8, 30, 'sha256'); + $this->assertEquals('46119246', $tfa->getCode($secret, 59)); + $this->assertEquals('68084774', $tfa->getCode($secret, 1111111109)); + $this->assertEquals('67062674', $tfa->getCode($secret, 1111111111)); + $this->assertEquals('91819424', $tfa->getCode($secret, 1234567890)); + $this->assertEquals('90698825', $tfa->getCode($secret, 2000000000)); + $this->assertEquals('77737706', $tfa->getCode($secret, 20000000000)); + } + + public function testKnownTestVectors_sha512() { + //Known test vectors for SHA512: https://tools.ietf.org/html/rfc6238#page-15 + $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA'; //== base32encode('1234567890123456789012345678901234567890123456789012345678901234') + $tfa = new TwoFactorAuth('Test', 8, 30, 'sha512'); + $this->assertEquals('90693936', $tfa->getCode($secret, 59)); + $this->assertEquals('25091201', $tfa->getCode($secret, 1111111109)); + $this->assertEquals('99943326', $tfa->getCode($secret, 1111111111)); + $this->assertEquals('93441116', $tfa->getCode($secret, 1234567890)); + $this->assertEquals('38618901', $tfa->getCode($secret, 2000000000)); + $this->assertEquals('47863826', $tfa->getCode($secret, 20000000000)); + } + + /** + * @requires function random_bytes + */ + public function testCSRNGProvidersReturnExpectedNumberOfBytes() { + $rng = new \RobThree\Auth\Providers\Rng\CSRNGProvider(); + foreach ($this->getRngTestLengths() as $l) + $this->assertEquals($l, strlen($rng->getRandomBytes($l))); + $this->assertEquals(true, $rng->isCryptographicallySecure()); + } + + /** + * @requires function hash_algos + * @requires function hash + */ + public function testHashRNGProvidersReturnExpectedNumberOfBytes() { + $rng = new \RobThree\Auth\Providers\Rng\HashRNGProvider(); + foreach ($this->getRngTestLengths() as $l) + $this->assertEquals($l, strlen($rng->getRandomBytes($l))); + $this->assertEquals(false, $rng->isCryptographicallySecure()); + } + + /** + * @requires function mcrypt_create_iv + */ + public function testMCryptRNGProvidersReturnExpectedNumberOfBytes() { + $rng = new \RobThree\Auth\Providers\Rng\MCryptRNGProvider(); + foreach ($this->getRngTestLengths() as $l) + $this->assertEquals($l, strlen($rng->getRandomBytes($l))); + $this->assertEquals(true, $rng->isCryptographicallySecure()); + } + + /** + * @requires function openssl_random_pseudo_bytes + */ + public function testStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes() { + $rng = new \RobThree\Auth\Providers\Rng\OpenSSLRNGProvider(true); + foreach ($this->getRngTestLengths() as $l) + $this->assertEquals($l, strlen($rng->getRandomBytes($l))); + $this->assertEquals(true, $rng->isCryptographicallySecure()); + } + + /** + * @requires function openssl_random_pseudo_bytes + */ + public function testNonStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes() { + $rng = new \RobThree\Auth\Providers\Rng\OpenSSLRNGProvider(false); + foreach ($this->getRngTestLengths() as $l) + $this->assertEquals($l, strlen($rng->getRandomBytes($l))); + $this->assertEquals(false, $rng->isCryptographicallySecure()); + } + + + private function getRngTestLengths() { + return array(1, 16, 32, 256); + } + + private function DecodeDataUri($datauri) { + if (preg_match('/data:(?P[\w\.\-\/]+);(?P\w+),(?P.*)/', $datauri, $m) === 1) { + return array( + 'mimetype' => $m['mimetype'], + 'encoding' => $m['encoding'], + 'data' => base64_decode($m['data']) + ); + } + return null; + } +} + +class TestRNGProvider implements IRNGProvider { + private $isSecure; + + function __construct($isSecure = false) { + $this->isSecure = $isSecure; + } + + public function getRandomBytes($bytecount) { + $result = ''; + for ($i=0; $i<$bytecount; $i++) + $result.=chr($i); + return $result; + + } + + public function isCryptographicallySecure() { + return $this->isSecure; + } +} + +class TestQrProvider implements IQRCodeProvider { + public function getQRCodeImage($qrtext, $size) { + return $qrtext . '@' . $size; + } + + public function getMimeType() { + return 'test/test'; + } +} + +class TestTimeProvider implements ITimeProvider { + private $time; + + function __construct($time) { + $this->time = $time; + } + + public function getTime() { + return $this->time; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/.gitignore b/data/web/inc/lib/vendor/yubico/u2flib-server/.gitignore new file mode 100644 index 00000000..0d968f1a --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/.gitignore @@ -0,0 +1,7 @@ +composer.lock +vendor/ +.*.swp +php-u2flib-server-*.tar.gz +php-u2flib-server-*.tar.gz.sig +apidocs/ +build/ diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/.travis.yml b/data/web/inc/lib/vendor/yubico/u2flib-server/.travis.yml new file mode 100644 index 00000000..781f2b84 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/.travis.yml @@ -0,0 +1,19 @@ +language: php +sudo: false +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + - hhvm-nightly +after_success: + - test -z $COVERALLS || (composer require satooshi/php-coveralls && vendor/bin/coveralls -v) +matrix: + include: + - php: 5.6 + env: COVERALLS=true + allow_failures: + - php: hhvm + - php: hhvm-nightly diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/BLURB b/data/web/inc/lib/vendor/yubico/u2flib-server/BLURB new file mode 100644 index 00000000..c5797421 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/BLURB @@ -0,0 +1,9 @@ +Author: Yubico +Basename: php-u2flib-server +Homepage: https://developers.yubico.com/php-u2flib-server +License: BSD-2-Clause +Name: Native U2F library in PHP +Project: php-u2flib-server +Summary: Native U2F library in PHP +Yubico-Category: U2F projects +Travis: https://travis-ci.org/Yubico/php-u2flib-server diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/COPYING b/data/web/inc/lib/vendor/yubico/u2flib-server/COPYING new file mode 100644 index 00000000..427c9175 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/COPYING @@ -0,0 +1,26 @@ +Copyright (c) 2014 Yubico AB +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/NEWS b/data/web/inc/lib/vendor/yubico/u2flib-server/NEWS new file mode 100644 index 00000000..0fffd587 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/NEWS @@ -0,0 +1,24 @@ +php-u2flib-server NEWS -- History of user-visible changes. + +* Version 1.0.0 (released 2016-02-19) + ** Give an early error on openssl < 1.0 + ** Support devices with initial counter 0 + ** Fixes to examples + ** Handle errorCode: 0 correctly + +* Version 0.1.0 (released 2015-03-03) + ** Use openssl for all crypto instead of third party extensions. + ** Properly check the request challenge on authenticate. + ** Switch from returning error codes to throwing exceptions. + ** Stop recommending composer for installation. + +* Version 0.0.2 (released 2014-10-24) + ** Refactor the API to return objects instead of encoded objects. + ** Add a second example that uses PDO to store registrations. + ** Add documentation to the API. + ** Check that randomness returned is good. + ** Drop the unneeded mcrypt extension. + ** More tests. + +* Version 0.0.1 (released 2014-10-16) + ** Initial release. diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/README b/data/web/inc/lib/vendor/yubico/u2flib-server/README new file mode 100644 index 00000000..0116a270 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/README @@ -0,0 +1,34 @@ +php-u2flib-server +----------------- + +image:https://travis-ci.org/Yubico/php-u2flib-server.svg?branch=master["Build Status", link="https://travis-ci.org/Yubico/php-u2flib-server"] +image:https://coveralls.io/repos/Yubico/php-u2flib-server/badge.svg?branch=master&service=github["Coverage", link="https://coveralls.io/github/Yubico/php-u2flib-server?branch=master"] +image:https://scrutinizer-ci.com/g/Yubico/php-u2flib-server/badges/quality-score.png?b=master["Scrutinizer Code Quality", link="https://scrutinizer-ci.com/g/Yubico/php-u2flib-server/?branch=master"] + +=== Introduction === + +Serverside U2F library for PHP. Provides functionality for registering +tokens and authentication with said tokens. + +To read more about U2F and how to use a U2F library, visit +link:http://developers.yubico.com/U2F[developers.yubico.com/U2F]. + +=== License === + +The project is licensed under a BSD license. See the file COPYING for +exact wording. For any copyright year range specified as YYYY-ZZZZ in +this package note that the range specifies every single year in that +closed interval. + +=== Dependencies === + +The only dependency is the openssl extension to PHP that has to be enabled. + +A composer.json is included in the distribution to make things simpler for +other project using composer. + +=== Tests === + +To run the test suite link:https://phpunit.de[PHPUnit] is required. To run it, type: + + $ phpunit diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/README.adoc b/data/web/inc/lib/vendor/yubico/u2flib-server/README.adoc new file mode 120000 index 00000000..100b9382 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/README.adoc @@ -0,0 +1 @@ +README \ No newline at end of file diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/apigen.neon b/data/web/inc/lib/vendor/yubico/u2flib-server/apigen.neon new file mode 100644 index 00000000..80d9e744 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/apigen.neon @@ -0,0 +1,12 @@ +destination: apidocs + +source: + - src/u2flib_server + +exclude: "*/tests/*" + +groups: none + +tree: false + +title: php-u2flib-server API diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/composer.json b/data/web/inc/lib/vendor/yubico/u2flib-server/composer.json new file mode 100644 index 00000000..f14a88f6 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/composer.json @@ -0,0 +1,13 @@ +{ + "name":"yubico/u2flib-server", + "description":"Library for U2F implementation", + "homepage":"https://developers.yubico.com/php-u2flib-server", + "license":"BSD-2-Clause", + "require": { + "ext-openssl":"*" + }, + "autoload": { + "classmap": ["src/"] + } +} + diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/do-source-release.sh b/data/web/inc/lib/vendor/yubico/u2flib-server/do-source-release.sh new file mode 100755 index 00000000..3c592ea3 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/do-source-release.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +set -e + +VERSION=$1 +PGP_KEYID=$2 + +if [ "x$PGP_KEYID" = "x" ]; then + echo "try with $0 VERSION PGP_KEYID" + echo "example: $0 0.0.1 B2168C0A" + exit +fi + +if ! head -3 NEWS | grep -q "Version $VERSION .released `date -I`"; then + echo "You need to update date/version in NEWS" + exit +fi + +if [ "x$YUBICO_GITHUB_REPO" = "x" ]; then + echo "you need to define YUBICO_GITHUB_REPO" + exit +fi + +releasename=php-u2flib-server-${VERSION} + +git push +git tag -u ${PGP_KEYID} -m $VERSION $VERSION +git push --tags +tmpdir=`mktemp -d /tmp/release.XXXXXX` +releasedir=${tmpdir}/${releasename} +mkdir -p $releasedir +git archive $VERSION --format=tar | tar -xC $releasedir +git2cl > $releasedir/ChangeLog +cd $releasedir +apigen +cd - +tar -cz --directory=$tmpdir --file=${releasename}.tar.gz $releasename +gpg --detach-sign --default-key $PGP_KEYID ${releasename}.tar.gz +$YUBICO_GITHUB_REPO/publish php-u2flib-server $VERSION ${releasename}.tar.gz* +rm -rf $tmpdir diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/examples/assets/u2f-api.js b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/assets/u2f-api.js new file mode 100644 index 00000000..0f06f50d --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/assets/u2f-api.js @@ -0,0 +1,651 @@ +// Copyright 2014-2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ + +'use strict'; + +/** Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * The U2F extension id + * @type {string} + * @const + */ +u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response' +}; + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + +/** + * A message type for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * signRequests: Array, + * registerRequests: ?Array, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.Request; + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.Response; + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string, + * appId: string + * }} + */ +u2f.RegisterRequest; + +/** + * Data object for a registration response. + * @typedef {{ + * registrationData: string, + * clientData: string + * }} + */ +u2f.RegisterResponse; + + +// Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format a return a sign request. + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ = + function(signRequests, timeoutSeconds, reqId) { + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + +/** + * Format a return a register request. + * @param {Array} signRequests + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ = + function(signRequests, registerRequests, timeoutSeconds, reqId) { + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } + }; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentLocation = /** @type {string} */ (message); + document.location = intentLocation; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } + }; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + responseObject['requestId'] = this.requestId_; + } + + /* Sign responses from the authenticator do not conform to U2F, + * convert to U2F here. */ + responseObject = this.doResponseFixups_(responseObject); + callback({'data': responseObject}); + }; + +/** + * Fixup the response provided by the Authenticator to conform with + * the U2F spec. + * @param {Object} responseData + * @return {Object} the U2F compliant response object + */ +u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ = + function(responseObject) { + if (responseObject.hasOwnProperty('responseData')) { + return responseObject; + } else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) { + // Only sign responses require fixups. If this is not a response + // to a sign request, then an internal error has occurred. + return { + 'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE, + 'responseData': { + 'errorCode': u2f.ErrorCodes.OTHER_ERROR, + 'errorMessage': 'Internal error: invalid response from Authenticator' + } + }; + } + + /* Non-conformant sign response, do fixups. */ + var encodedChallengeObject = responseObject['challenge']; + if (typeof encodedChallengeObject !== 'undefined') { + var challengeObject = JSON.parse(atob(encodedChallengeObject)); + var serverChallenge = challengeObject['challenge']; + var challengesList = this.requestObject_['signData']; + var requestChallengeObject = null; + for (var i = 0; i < challengesList.length; i++) { + var challengeObject = challengesList[i]; + if (challengeObject['keyHandle'] == responseObject['keyHandle']) { + requestChallengeObject = challengeObject; + break; + } + } + } + var responseData = { + 'errorCode': responseObject['resultCode'], + 'keyHandle': responseObject['keyHandle'], + 'signatureData': responseObject['signature'], + 'clientData': encodedChallengeObject + }; + return { + 'type': u2f.MessageTypes.U2F_SIGN_RESPONSE, + 'responseData': responseData, + 'requestId': responseObject['requestId'] + } + }; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Format a return a sign request. + * @param {Array} signRequests + * @param {number} timeoutSeconds (ignored for now) + * @param {number} reqId + * @return {string} + */ +u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ = + function(signRequests, timeoutSeconds, reqId) { + if (!signRequests || signRequests.length == 0) { + return null; + } + /* TODO(fixme): stash away requestId, as the authenticator app does + * not return it for sign responses. */ + this.requestId_ = reqId; + /* TODO(fixme): stash away the signRequests, to deal with the legacy + * response format returned by the Authenticator app. */ + this.requestObject_ = { + 'type': u2f.MessageTypes.U2F_SIGN_REQUEST, + 'signData': signRequests, + 'requestId': reqId, + 'timeout': timeoutSeconds + }; + + var appId = signRequests[0]['appId']; + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.appId=' + encodeURIComponent(appId) + + ';S.eventId=' + reqId + + ';S.challenges=' + + encodeURIComponent( + JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end'; + return intentUrl; + }; + +/** + * Get the browser data objects from the challenge list + * @param {Array} challenges list of challenges + * @return {Array} list of browser data objects + * @private + */ +u2f.WrappedAuthenticatorPort_ + .prototype.getBrowserDataList_ = function(challenges) { + return challenges + .map(function(challenge) { + var browserData = { + 'typ': 'navigator.id.getAssertion', + 'challenge': challenge['challenge'] + }; + var challengeObject = { + 'challenge' : browserData, + 'keyHandle' : challenge['keyHandle'] + }; + return challengeObject; + }); +}; + +/** + * Format a return a register request. + * @param {Array} signRequests + * @param {Array} enrollChallenges + * @param {number} timeoutSeconds (ignored for now) + * @param {number} reqId + * @return {Object} + */ +u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ = + function(signRequests, enrollChallenges, timeoutSeconds, reqId) { + if (!enrollChallenges || enrollChallenges.length == 0) { + return null; + } + // Assume the appId is the same for all enroll challenges. + var appId = enrollChallenges[0]['appId']; + var registerRequests = []; + for (var i = 0; i < enrollChallenges.length; i++) { + var registerRequest = { + 'challenge': enrollChallenges[i]['challenge'], + 'version': enrollChallenges[i]['version'] + }; + if (enrollChallenges[i]['appId'] != appId) { + // Only include the appId when it differs from the first appId. + registerRequest['appId'] = enrollChallenges[i]['appId']; + } + registerRequests.push(registerRequest); + } + var registeredKeys = []; + if (signRequests) { + for (i = 0; i < signRequests.length; i++) { + var key = { + 'keyHandle': signRequests[i]['keyHandle'], + 'version': signRequests[i]['version'] + }; + // Only include the appId when it differs from the appId that's + // being registered now. + if (signRequests[i]['appId'] != appId) { + key['appId'] = signRequests[i]['appId']; + } + registeredKeys.push(key); + } + } + var request = { + 'type': u2f.MessageTypes.U2F_REGISTER_REQUEST, + 'appId': appId, + 'registerRequests': registerRequests, + 'registeredKeys': registeredKeys, + 'requestId': reqId, + 'timeoutSeconds': timeoutSeconds + }; + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(request)) + + ';end'; + /* TODO(fixme): stash away requestId, this is is not necessary for + * register requests, but here to keep parity with sign. + */ + this.requestId_ = reqId; + return intentUrl; + }; + + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +// High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {Array} signRequests + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(signRequests, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = port.formatSignRequest_(signRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {Array} registerRequests + * @param {Array} signRequests + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(registerRequests, signRequests, + callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = port.formatRegisterRequest_( + signRequests, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; \ No newline at end of file diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/examples/cli/u2f-server.php b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/cli/u2f-server.php new file mode 100755 index 00000000..8acb66a4 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/cli/u2f-server.php @@ -0,0 +1,83 @@ +#!/usr/bin/php +getRegisterData(); +} elseif($mode === "authenticate") { + $challenge = $u2f->getAuthenticateData($regs); +} + +print json_encode($challenge[0]) . "\n"; +$response = fgets(STDIN); + +if($mode === "register") { + $result = $u2f->doRegister($challenge[0], json_decode($response)); +} elseif($mode === "authenticate") { + $result = $u2f->doAuthenticate($challenge, $regs, json_decode($response)); +} + +print json_encode($result) . "\n"; + +?> diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/examples/localstorage/index.php b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/localstorage/index.php new file mode 100644 index 00000000..d840dd36 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/localstorage/index.php @@ -0,0 +1,186 @@ + + + + PHP U2F Demo + + + + + + + +
+ + + + + + +
+ +

+ 0 Authenticators currently registered. +

+ + + + diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/examples/pdo/index.php b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/pdo/index.php new file mode 100644 index 00000000..c04d63e2 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/pdo/index.php @@ -0,0 +1,204 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); + +$pdo->exec("create table if not exists users (id integer primary key, name varchar(255))"); +$pdo->exec("create table if not exists registrations (id integer primary key, user_id integer, keyHandle varchar(255), publicKey varchar(255), certificate text, counter integer)"); + +$scheme = isset($_SERVER['HTTPS']) ? "https://" : "http://"; +$u2f = new u2flib_server\U2F($scheme . $_SERVER['HTTP_HOST']); + +session_start(); + +function createAndGetUser($name) { + global $pdo; + $sel = $pdo->prepare("select * from users where name = ?"); + $sel->execute(array($name)); + $user = $sel->fetch(); + if(!$user) { + $ins = $pdo->prepare("insert into users (name) values(?)"); + $ins->execute(array($name)); + $sel->execute(array($name)); + $user = $sel->fetch(); + } + return $user; +} + +function getRegs($user_id) { + global $pdo; + $sel = $pdo->prepare("select * from registrations where user_id = ?"); + $sel->execute(array($user_id)); + return $sel->fetchAll(); +} + +function addReg($user_id, $reg) { + global $pdo; + $ins = $pdo->prepare("insert into registrations (user_id, keyHandle, publicKey, certificate, counter) values (?, ?, ?, ?, ?)"); + $ins->execute(array($user_id, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter)); +} + +function updateReg($reg) { + global $pdo; + $upd = $pdo->prepare("update registrations set counter = ? where id = ?"); + $upd->execute(array($reg->counter, $reg->id)); +} + +?> + + + + PHP U2F example + + + + + + + +
+ username:
+ register:
+ authenticate:
+ + + +
+ + + diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/phpunit.xml b/data/web/inc/lib/vendor/yubico/u2flib-server/phpunit.xml new file mode 100644 index 00000000..603e6935 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/phpunit.xml @@ -0,0 +1,9 @@ + + + . + + + + + diff --git a/data/web/inc/lib/U2F.php b/data/web/inc/lib/vendor/yubico/u2flib-server/src/u2flib_server/U2F.php similarity index 100% rename from data/web/inc/lib/U2F.php rename to data/web/inc/lib/vendor/yubico/u2flib-server/src/u2flib_server/U2F.php diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/tests/certs/yubico-u2f-ca-1.pem b/data/web/inc/lib/vendor/yubico/u2flib-server/tests/certs/yubico-u2f-ca-1.pem new file mode 100644 index 00000000..15a1dc28 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/tests/certs/yubico-u2f-ca-1.pem @@ -0,0 +1,19 @@ +-----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----- diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/tests/u2flib_test.php b/data/web/inc/lib/vendor/yubico/u2flib-server/tests/u2flib_test.php new file mode 100644 index 00000000..56af62bf --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/tests/u2flib_test.php @@ -0,0 +1,296 @@ +u2f = new u2flib_server\U2F("http://demo.example.com"); + } + + public function testGetRegisterData() { + list($reg, $signData) = $this->u2f->getRegisterData(); + $this->assertJsonStringEqualsJsonString(json_encode(array()), json_encode($signData)); + $this->assertEquals('U2F_V2', $reg->version); + $this->assertObjectHasAttribute('challenge', $reg); + $this->assertEquals('http://demo.example.com', $reg->appId); + } + + public function testDoRegister() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9", "errorCode": 0 }'); + $reg = $this->u2f->doRegister($req, $resp); + $this->assertEquals('CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w', $reg->keyHandle); + $this->assertEquals('BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y/yaFORPUe3c=', $reg->publicKey); + $this->assertEquals('MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp/VRZHOwd2NZNzpnB9ePNKvUaWCGK/gN+cynnYFdwJ75iSgMVYb/RnFcdPwnsBzBU68hbhTnu/FvJxWo7rZJ2q7qXpA10eLVXJr4/4oSXEk9I/0IIHqOP98Ck/fAoI5gYI7ygndyqoPJ/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh/h7oKEKamCWk19dJp5jHQmumkHlvQhH/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg/0J+xOb4zl6a1z65nae4OTj7628/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==', $reg->certificate); + $this->assertLessThan(0, $reg->counter); + } + + public function testDoRegisterNoCert() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $reg = $this->u2f->doRegister($req, $resp, false); + $this->assertEquals('CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w', $reg->keyHandle); + $this->assertEquals('BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y/yaFORPUe3c=', $reg->publicKey); + $this->assertEquals('', $reg->certificate); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_ATTESTATION_VERIFICATION + */ + public function testDoRegisterAttestFail() { + $this->u2f = new u2flib_server\U2F("http://demo.example.com", __DIR__ . "/../tests/certs"); + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp, true); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_ATTESTATION_SIGNATURE + */ + public function testDoRegisterFail2() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8NwW=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp, false); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_UNMATCHED_CHALLENGE + */ + public function testDoRegisterFail() { + $req = json_decode('{"version":"U2F_V2","challenge":"YKA0X075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp, false); + } + + public function testDoRegisterAttest() { + $this->u2f = new u2flib_server\U2F("http://demo.example.com", __DIR__ . "/../tests/certs"); + $req = json_decode('{"version":"U2F_V2","challenge":"5CBRhGBb2CXSum71GNREBGft7yz9g1jZO7JTkHGFsVY","appId":"http:\/\/demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQRX1gfcG-ofTlk9rjB9spsIMrmT9ba0DLto5fzk8FDB05ModNU2sWAqoQRemYiUrILQdbNGpN_aHA0_oq8kcd_XQCK-Ut0PWaOtz43t0aAV04U788e-dvpeqLtHxtINjgmutKM8_GJQ7F-3W0dogUjSANuRYRdkkSEHPcVdLSkpyfowggIbMIIBBaADAgECAgRAxBIlMAsGCSqGSIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTA4NjU5MTUyNTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABK2iSVV7KGNEdPE-oHGvobNnHVw6ZZ6vB3jNIYB1C4t32OucHzMweHqM5CAMSMDHtfp1vuJYaiQSk7jb6M48WtejEjAQMA4GCisGAQQBgsQKAQEEADALBgkqhkiG9w0BAQsDggEBAVg0BoEHEEp4LJLYPYFACRGS8WZiXkCA8crYLgGnzvfKXwPwyKJlUzYxxv5xoRrl5zjkIUXhZ4mnHZVsnj9EY_VGDuRRzKX7YtxTZpFZn7ej3abjLhckTkkQ_AhUkmP7VuK2AWLgYsS8ejGUqughBsKvh_84uxTAEr5BS-OGg2yi7UIjd8W0nOCc6EN8d_8wCiPOjt2Y_-TKpLLTXKszk4UnWNzRdxBThmBBprJBZbF1VyVRvJm5yRLBpth3G8KMvrt4Nu3Ecoj_Q154IJpWe1Dp1upDFLOG9nWCRQk25Y264k9BDISfqs-wHvUjIo2iDnKl5UVoauTWaT7M6KuEwl4wRAIgYUVjS_yTwJAtF35glSbf9Et-5tJzlHOeAqmbACd6pwsCIE0MkTR5XNQoO4XqDaUZCXmadWu8yU1gfE7AJI9JUUcc", "clientData": "eyAiY2hhbGxlbmdlIjogIjVDQlJoR0JiMkNYU3VtNzFHTlJFQkdmdDd5ejlnMWpaTzdKVGtIR0ZzVlkiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $reg = $this->u2f->doRegister($req, $resp, true); + $this->assertEquals('Ir5S3Q9Zo63Pje3RoBXThTvzx752-l6ou0fG0g2OCa60ozz8YlDsX7dbR2iBSNIA25FhF2SRIQc9xV0tKSnJ-g', $reg->keyHandle); + $this->assertEquals('BFfWB9wb6h9OWT2uMH2ymwgyuZP1trQMu2jl/OTwUMHTkyh01TaxYCqhBF6ZiJSsgtB1s0ak39ocDT+iryRx39c=', $reg->publicKey); + $this->assertEquals('MIICGzCCAQWgAwIBAgIEQMQSJTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDEwODY1OTE1MjUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAStoklVeyhjRHTxPqBxr6GzZx1cOmWerwd4zSGAdQuLd9jrnB8zMHh6jOQgDEjAx7X6db7iWGokEpO42+jOPFrXoxIwEDAOBgorBgEEAYLECgEBBAAwCwYJKoZIhvcNAQELA4IBAQBYNAaBBxBKeCyS2D2BQAkRkvFmYl5AgPHK2C4Bp873yl8D8MiiZVM2Mcb+caEa5ec45CFF4WeJpx2VbJ4/RGP1Rg7kUcyl+2LcU2aRWZ+3o92m4y4XJE5JEPwIVJJj+1bitgFi4GLEvHoxlKroIQbCr4f/OLsUwBK+QUvjhoNsou1CI3fFtJzgnOhDfHf/MAojzo7dmP/kyqSy01yrM5OFJ1jc0XcQU4ZgQaayQWWxdVclUbyZuckSwabYdxvCjL67eDbtxHKI/0NeeCCaVntQ6dbqQxSzhvZ1gkUJNuWNuuJPQQyEn6rPsB71IyKNog5ypeVFaGrk1mk+zOirhMJe', $reg->certificate); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_PUBKEY_DECODE + */ + public function testDoRegisterBadKeyInCert() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABdsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_PUBKEY_DECODE + */ + public function testDoRegisterBadKey() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQMtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $request of doRegister() method only accepts object. + */ + public function testDoRegisterInvalidRequest() { + $req = 'request'; + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $response of doRegister() method only accepts object. + */ + public function testDoRegisterInvalidResponse() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = 'response'; + $this->u2f->doRegister($req, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_BAD_UA_RETURNING + */ + public function testDoRegisterUAError() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{"errorCode": "4"}'); + $this->u2f->doRegister($req, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $include_cert of doRegister() method only accepts boolean. + */ + public function testDoRegisterInvalidInclude_cert() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp, 'bar'); + } + + public function testGetAuthenticateData() { + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); + $data = $this->u2f->getAuthenticateData($regs); + $inst = $data[0]; + $this->assertEquals("U2F_V2", $inst->version); + $this->assertObjectHasAttribute("challenge", $inst); + $this->assertEquals('CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w', $inst->keyHandle); + $this->assertEquals('http://demo.example.com', $inst->appId); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $registrations of getAuthenticateData() method only accepts array of object. + */ + public function testGetAuthenticateDataInvalidRegistrations2() { + $regs = array('YubiKey NEO', 'YubiKey Standard'); + $data = $this->u2f->getAuthenticateData($regs); + } + + public function testDoAuthenticate() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w", "errorCode": 0 }'); + $data = $this->u2f->doAuthenticate($reqs, $regs, $resp); + $this->assertEquals(4, $data->counter); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_COUNTER_TOO_LOW + */ + public function testDoAuthenticateCtrFail() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":5}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_AUTHENTICATION_FAILURE + */ + public function testDoAuthenticateFail() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAnG==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_NO_MATCHING_REQUEST + */ + public function testDoAuthenticateWrongReq() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"cTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_NO_MATCHING_REGISTRATION + */ + public function testDoAuthenticateWrongReg() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"cTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_PUBKEY_DECODE + */ + public function testDoAuthenticateBadKey() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"bC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $requests of doAuthenticate() method only accepts array of object. + */ + public function testDoAuthenticateInvalidRequests2() { + $reqs = array('YubiKey NEO', 'YubiKey Standard'); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $registrations of doAuthenticate() method only accepts array of object. + */ + public function testDoAuthenticateInvalidRegistrations2() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array('YubiKey NEO', 'YubiKey Standard'); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $response of doAuthenticate() method only accepts object. + */ + public function testDoAuthenticateInvalidResponse() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); + $resp = 'Response'; + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_BAD_UA_RETURNING + */ + public function testDoAuthenticateUAError() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); + $resp = json_decode('{"errorCode": "5"}'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } +} + +?> diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index c766e780..b39f755a 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -24,9 +24,10 @@ if (file_exists('./inc/vars.local.inc.php')) { // Yubi OTP API require_once 'inc/lib/Yubico.php'; -// U2F API -require_once 'inc/lib/U2F.php'; +// U2F API + T/HOTP API +require_once 'inc/lib/vendor/autoload.php'; $u2f = new u2flib_server\U2F('https://' . $_SERVER['SERVER_NAME']); +$tfa = new RobThree\Auth\TwoFactorAuth('mailcow UI'); // PDO // Calculate offset diff --git a/data/web/inc/tfa_modals.php b/data/web/inc/tfa_modals.php index d6956724..f77ee702 100644 --- a/data/web/inc/tfa_modals.php +++ b/data/web/inc/tfa_modals.php @@ -1,3 +1,6 @@ +