public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
[Web] add cors to json_api
192f67cd
data/web/admin.php | 6 ++
data/web/inc/functions.inc.php | 104 ++++++++++++++++++++++++
data/web/json_api.php | 31 +++----
data/web/lang/lang.de-de.json | 2 +
data/web/lang/lang.en-gb.json | 5 ++
data/web/templates/admin/tab-config-admins.twig | 33 ++++++++
6 files changed, 167 insertions(+), 14 deletions(-)
Diff
diff --git a/data/web/admin.php b/data/web/admin.php
index cd3eb890..14cb89f5 100644
--- a/data/web/admin.php
+++ b/data/web/admin.php
@@ -80,6 +80,11 @@ foreach ($RSPAMD_MAPS['regex'] as $rspamd_regex_desc => $rspamd_regex_map) {
];
}
+// cors settings
+$cors_settings = cors('get');
+$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
+$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
+
$template = 'admin.twig';
$template_data = [
'tfa_data' => $tfa_data,
@@ -106,6 +111,7 @@ $template_data = [
'ip_check' => customize('get', 'ip_check'),
'password_complexity' => password_complexity('get'),
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
+ 'cors_settings' => $cors_settings,
'lang_admin' => json_encode($lang['admin']),
'lang_datatables' => json_encode($lang['datatables'])
];
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php
index 4dc2418c..bcfe1243 100644
--- a/data/web/inc/functions.inc.php
+++ b/data/web/inc/functions.inc.php
@@ -2131,6 +2131,110 @@ function rspamd_ui($action, $data = null) {
break;
}
}
+function cors($action, $data = null) {
+ global $redis;
+
+ switch ($action) {
+ case "edit":
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $data),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+
+ $allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']);
+ $allowed_origins = !is_array($allowed_origins) ? array_map('trim', preg_split( "/( |,|;|\n)/", $allowed_origins)) : $allowed_origins;
+
+ $allowed_methods = isset($data['allowed_methods']) ? $data['allowed_methods'] : array('GET', 'POST', 'PUT', 'DELETE');
+ $allowed_methods = !is_array($allowed_methods) ? array_map('trim', preg_split( "/( |,|;|\n)/", $allowed_methods)) : $allowed_methods;
+ $available_methods = array('GET', 'POST', 'PUT', 'DELETE', 'OPTION');
+ foreach ($allowed_methods as $method) {
+ if (!in_array($method, $available_methods)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $data),
+ 'msg' => 'cors_invalid_method'
+ );
+ return false;
+ }
+ }
+
+ try {
+ $redis->hMSet('CORS_SETTINGS', array(
+ 'allowed_origins' => implode(', ', $allowed_origins),
+ 'allowed_methods' => implode(', ', $allowed_methods)
+ ));
+ } catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $data),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $action, $data),
+ 'msg' => 'cors_headers_edited'
+ );
+ return true;
+ break;
+ case "get":
+ try {
+ $cors_settings = $redis->hMGet('CORS_SETTINGS', array('allowed_origins', 'allowed_methods'));
+ } catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $data),
+ 'msg' => array('redis_error', $e)
+ );
+ }
+
+ $cors_settings = !$cors_settings ? array('allowed_origins' => $_SERVER['SERVER_NAME'], 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
+ $cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? $_SERVER['SERVER_NAME'] : $cors_settings['allowed_origins'];
+ $cors_settings['allowed_methods'] = empty($cors_settings['allowed_methods']) ? 'GET, POST, PUT, DELETE, OPTION' : $cors_settings['allowed_methods'];
+
+ return $cors_settings;
+ break;
+ case "set_headers":
+ $cors_settings = cors('get');
+ // check if requested origin is in allowed origins
+ $allowed_origins = explode(', ', $cors_settings['allowed_origins']);
+ $cors_settings['allowed_origins'] = $allowed_origins[0];
+ if (in_array('*', $allowed_origins)){
+ $cors_settings['allowed_origins'] = '*';
+ } else if (in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
+ $cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
+ }
+ // always allow OPTIONS for preflight request
+ $cors_settings["allowed_methods"] = empty($cors_settings["allowed_methods"]) ? 'OPTIONS' : $cors_settings["allowed_methods"] . ', ' . 'OPTIONS';
+
+ header('Access-Control-Allow-Origin: ' . $cors_settings['allowed_origins']);
+ header('Access-Control-Allow-Methods: '. $cors_settings['allowed_methods']);
+ header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
+
+ // Access-Control settings requested, this is just a preflight request
+ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' &&
+ isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) &&
+ isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
+
+ $allowed_methods = explode(', ', $cors_settings["allowed_methods"]);
+ if (in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $allowed_methods, true))
+ // method allowed send 200 OK
+ http_response_code(200);
+ else
+ // method not allowed send 405 METHOD NOT ALLOWED
+ http_response_code(405);
+
+ exit;
+ }
+ break;
+ }
+}
function get_logs($application, $lines = false) {
if ($lines === false) {
diff --git a/data/web/json_api.php b/data/web/json_api.php
index ec028fe4..16c78baf 100644
--- a/data/web/json_api.php
+++ b/data/web/json_api.php
@@ -2,9 +2,9 @@
/*
see /api
*/
-
-header('Content-Type: application/json');
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+cors("set_headers");
+header('Content-Type: application/json');
error_reporting(0);
function api_log($_data) {
@@ -288,18 +288,18 @@ if (isset($_GET['query'])) {
case "domain-admin":
process_add_return(domain_admin('add', $attr));
break;
- case "sso":
- switch ($object) {
- case "domain-admin":
- $data = domain_admin_sso('issue', $attr);
- if($data) {
- echo json_encode($data);
- exit(0);
- }
- process_add_return($data);
- break;
- }
- break;
+ case "sso":
+ switch ($object) {
+ case "domain-admin":
+ $data = domain_admin_sso('issue', $attr);
+ if($data) {
+ echo json_encode($data);
+ exit(0);
+ }
+ process_add_return($data);
+ break;
+ }
+ break;
case "admin":
process_add_return(admin('add', $attr));
break;
@@ -1946,6 +1946,9 @@ if (isset($_GET['query'])) {
process_edit_return(edit_user_account($attr));
}
break;
+ case "cors":
+ process_edit_return(cors('edit', $attr));
+ break;
// return no route found if no case is matched
default:
http_response_code(404);
diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json
index 4bd4b3fa..d994cd13 100644
--- a/data/web/lang/lang.de-de.json
+++ b/data/web/lang/lang.de-de.json
@@ -147,6 +147,7 @@
"change_logo": "Logo ändern",
"configuration": "Konfiguration",
"convert_html_to_text": "Konvertiere HTML zu reinem Text",
+ "cors_settings": "CORS Einstellungen",
"credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.",
"customer_id": "Kunde",
"customize": "UI-Anpassung",
@@ -996,6 +997,7 @@
"bcc_deleted": "BCC-Map-Einträge gelöscht: %s",
"bcc_edited": "BCC-Map-Eintrag %s wurde geändert",
"bcc_saved": "BCC- Map-Eintrag wurde gespeichert",
+ "cors_headers_edited": "CORS headers wurden erfolgreich gespeichert",
"db_init_complete": "Datenbankinitialisierung abgeschlossen",
"delete_filter": "Filter-ID %s wurde gelöscht",
"delete_filters": "Filter gelöscht: %s",
diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json
index df83987c..cc0d8a69 100644
--- a/data/web/lang/lang.en-gb.json
+++ b/data/web/lang/lang.en-gb.json
@@ -133,6 +133,8 @@
"admins": "Administrators",
"admins_ldap": "LDAP Administrators",
"advanced_settings": "Advanced settings",
+ "allowed_methods": "Access-Control-Allow-Methods",
+ "allowed_origins": "Access-Control-Allow-Origin",
"api_allow_from": "Allow API access from these IPs/CIDR network notations",
"api_info": "The API is a work in progress. The documentation can be found at <a href=\"/api\">/api</a>",
"api_key": "API key",
@@ -149,6 +151,7 @@
"change_logo": "Change logo",
"configuration": "Configuration",
"convert_html_to_text": "Convert HTML to plain text",
+ "cors_settings": "CORS Settings",
"credentials_transport_warning": "<b>Warning</b>: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.",
"customer_id": "Customer ID",
"customize": "Customize",
@@ -358,6 +361,7 @@
"bcc_exists": "A BCC map %s exists for type %s",
"bcc_must_be_email": "BCC destination %s is not a valid email address",
"comment_too_long": "Comment too long, max 160 chars allowed",
+ "cors_invalid_method": "Invalid Allow-Method specified",
"defquota_empty": "Default quota per mailbox must not be 0.",
"demo_mode_enabled": "Demo Mode is enabled",
"description_invalid": "Resource description for %s is invalid",
@@ -1003,6 +1007,7 @@
"bcc_deleted": "BCC map entries deleted: %s",
"bcc_edited": "BCC map entry %s edited",
"bcc_saved": "BCC map entry saved",
+ "cors_headers_edited": "CORS headers successfully set.",
"db_init_complete": "Database initialization completed",
"delete_filter": "Deleted filters ID %s",
"delete_filters": "Deleted filters: %s",
diff --git a/data/web/templates/admin/tab-config-admins.twig b/data/web/templates/admin/tab-config-admins.twig
index 0221a8e7..51b595c2 100644
--- a/data/web/templates/admin/tab-config-admins.twig
+++ b/data/web/templates/admin/tab-config-admins.twig
@@ -97,6 +97,39 @@
<div class="col-lg-12">
<p class="text-muted">{{ lang.admin.api_info|raw }}</p>
</div>
+ <div class="col-lg-12">
+ <div class="card mb-3">
+ <div class="card-header">
+ <h4 class="card-title"><i class="bi bi-file-earmark-arrow-down"></i> {{ lang.admin.cors_settings }}</h4>
+ </div>
+ <div class="card-body">
+ <form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" data-id="editcors" method="post">
+ <div class="row mb-4">
+ <label class="control-label col-sm-2 mb-4" for="allowed_origins">{{ lang.admin.allowed_origins }}</label>
+ <div class="col-sm-9 mb-4">
+ <textarea class="form-control textarea-code" rows="7" name="allowed_origins" id="allowed_origins">{{ cors_settings.allowed_origins }}</textarea>
+ </div>
+ </div>
+ <div class="row mb-4">
+ <label class="control-label col-sm-2" for="allowed_methods">{{ lang.admin.allowed_methods }}</label>
+ <div class="col-sm-9">
+ <select name="allowed_methods" id="allowed_methods" multiple class="form-control">
+ <option value="POST"{% if "POST" in cors_settings.allowed_methods %} selected{% endif %}>POST</option>
+ <option value="GET"{% if "GET" in cors_settings.allowed_methods %} selected{% endif %}>GET</option>
+ <option value="DELETE"{% if "DELETE" in cors_settings.allowed_methods %} selected{% endif %}>DELETE</option>
+ <option value="PUT"{% if "PUT" in cors_settings.allowed_methods %} selected{% endif %}>PUT</option>
+ </select>
+ </div>
+ </div>
+ <div class="row mb-4">
+ <div class="offset-sm-2 col-sm-9">
+ <button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-success" data-item="cors" data-api-url="edit/cors" data-id="editcors" data-action="edit_selected" href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
<div class="col-lg-6">
<div class="card mb-3">
<div class="card-header">