diff --git a/data/web/admin.php b/data/web/admin.php index cd3eb89045..14cb89f586 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -80,6 +80,11 @@ ]; } +// 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 @@ '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 4dc2418cf9..901b894ec5 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -2131,6 +2131,120 @@ 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_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins; + foreach ($allowed_origins as $origin) { + if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $action, $data), + 'msg' => 'cors_invalid_origin' + ); + return false; + } + } + + $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'); + 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 ec028fe4b7..16c78baf75 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 @@ function process_add_return($return) { 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 @@ function process_edit_return($return) { 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 6b280bbb8f..d6f79dc58d 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": "Warnung: 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", @@ -358,6 +359,8 @@ "bcc_exists": "Ein BCC-Map-Eintrag %s existiert bereits als Typ %s", "bcc_must_be_email": "BCC-Ziel %s ist keine gültige E-Mail-Adresse", "comment_too_long": "Kommentarfeld darf maximal 160 Zeichen enthalten", + "cors_invalid_method": "Allow-Methods enthält eine ungültige Methode", + "cors_invalid_origin": "Allow-Origins enthält eine ungültige Origin", "defquota_empty": "Standard-Quota darf nicht 0 sein", "demo_mode_enabled": "Demo Mode ist aktiviert", "description_invalid": "Ressourcenbeschreibung für %s ist ungültig", @@ -998,6 +1001,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 Einstellungen 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 e53fe896c6..28ff19b81a 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 /api", "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": "Warning: 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,8 @@ "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", + "cors_invalid_origin": "Invalid Allow-Origin 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", @@ -1005,6 +1010,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 settings have been saved", "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 0221a8e71f..51b595c2e7 100644 --- a/data/web/templates/admin/tab-config-admins.twig +++ b/data/web/templates/admin/tab-config-admins.twig @@ -97,6 +97,39 @@

{{ lang.admin.api_info|raw }}

+
+
+
+

{{ lang.admin.cors_settings }}

+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+