浏览代码

[Web] add cors to json_api

FreddleSpl0it 2 年之前
父节点
当前提交
192f67cd41

+ 6 - 0
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 = 'admin.twig';
 $template_data = [
 $template_data = [
   'tfa_data' => $tfa_data,
   'tfa_data' => $tfa_data,
@@ -106,6 +111,7 @@ $template_data = [
   'ip_check' => customize('get', 'ip_check'),
   'ip_check' => customize('get', 'ip_check'),
   'password_complexity' => password_complexity('get'),
   'password_complexity' => password_complexity('get'),
   'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
   'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
+  'cors_settings' => $cors_settings,
   'lang_admin' => json_encode($lang['admin']),
   'lang_admin' => json_encode($lang['admin']),
   'lang_datatables' => json_encode($lang['datatables'])
   'lang_datatables' => json_encode($lang['datatables'])
 ];
 ];

+ 104 - 0
data/web/inc/functions.inc.php

@@ -2131,6 +2131,110 @@ function rspamd_ui($action, $data = null) {
     break;
     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) {
 function get_logs($application, $lines = false) {
   if ($lines === false) {
   if ($lines === false) {

+ 17 - 14
data/web/json_api.php

@@ -2,9 +2,9 @@
 /*
 /*
    see /api
    see /api
 */
 */
-
-header('Content-Type: application/json');
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+cors("set_headers");
+header('Content-Type: application/json');
 error_reporting(0);
 error_reporting(0);
 
 
 function api_log($_data) {
 function api_log($_data) {
@@ -288,18 +288,18 @@ if (isset($_GET['query'])) {
         case "domain-admin":
         case "domain-admin":
           process_add_return(domain_admin('add', $attr));
           process_add_return(domain_admin('add', $attr));
         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 "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":
         case "admin":
           process_add_return(admin('add', $attr));
           process_add_return(admin('add', $attr));
         break;
         break;
@@ -1946,6 +1946,9 @@ if (isset($_GET['query'])) {
             process_edit_return(edit_user_account($attr));
             process_edit_return(edit_user_account($attr));
           }
           }
         break;
         break;
+        case "cors":
+          process_edit_return(cors('edit', $attr));
+        break;
         // return no route found if no case is matched
         // return no route found if no case is matched
         default:
         default:
           http_response_code(404);
           http_response_code(404);

+ 2 - 0
data/web/lang/lang.de-de.json

@@ -147,6 +147,7 @@
         "change_logo": "Logo ändern",
         "change_logo": "Logo ändern",
         "configuration": "Konfiguration",
         "configuration": "Konfiguration",
         "convert_html_to_text": "Konvertiere HTML zu reinem Text",
         "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.",
         "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",
         "customer_id": "Kunde",
         "customize": "UI-Anpassung",
         "customize": "UI-Anpassung",
@@ -996,6 +997,7 @@
         "bcc_deleted": "BCC-Map-Einträge gelöscht: %s",
         "bcc_deleted": "BCC-Map-Einträge gelöscht: %s",
         "bcc_edited": "BCC-Map-Eintrag %s wurde geändert",
         "bcc_edited": "BCC-Map-Eintrag %s wurde geändert",
         "bcc_saved": "BCC- Map-Eintrag wurde gespeichert",
         "bcc_saved": "BCC- Map-Eintrag wurde gespeichert",
+        "cors_headers_edited": "CORS headers wurden erfolgreich gespeichert",
         "db_init_complete": "Datenbankinitialisierung abgeschlossen",
         "db_init_complete": "Datenbankinitialisierung abgeschlossen",
         "delete_filter": "Filter-ID %s wurde gelöscht",
         "delete_filter": "Filter-ID %s wurde gelöscht",
         "delete_filters": "Filter gelöscht: %s",
         "delete_filters": "Filter gelöscht: %s",

+ 5 - 0
data/web/lang/lang.en-gb.json

@@ -133,6 +133,8 @@
         "admins": "Administrators",
         "admins": "Administrators",
         "admins_ldap": "LDAP Administrators",
         "admins_ldap": "LDAP Administrators",
         "advanced_settings": "Advanced settings",
         "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_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_info": "The API is a work in progress. The documentation can be found at <a href=\"/api\">/api</a>",
         "api_key": "API key",
         "api_key": "API key",
@@ -149,6 +151,7 @@
         "change_logo": "Change logo",
         "change_logo": "Change logo",
         "configuration": "Configuration",
         "configuration": "Configuration",
         "convert_html_to_text": "Convert HTML to plain text",
         "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.",
         "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",
         "customer_id": "Customer ID",
         "customize": "Customize",
         "customize": "Customize",
@@ -358,6 +361,7 @@
         "bcc_exists": "A BCC map %s exists for type %s",
         "bcc_exists": "A BCC map %s exists for type %s",
         "bcc_must_be_email": "BCC destination %s is not a valid email address",
         "bcc_must_be_email": "BCC destination %s is not a valid email address",
         "comment_too_long": "Comment too long, max 160 chars allowed",
         "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.",
         "defquota_empty": "Default quota per mailbox must not be 0.",
         "demo_mode_enabled": "Demo Mode is enabled",
         "demo_mode_enabled": "Demo Mode is enabled",
         "description_invalid": "Resource description for %s is invalid",
         "description_invalid": "Resource description for %s is invalid",
@@ -1003,6 +1007,7 @@
         "bcc_deleted": "BCC map entries deleted: %s",
         "bcc_deleted": "BCC map entries deleted: %s",
         "bcc_edited": "BCC map entry %s edited",
         "bcc_edited": "BCC map entry %s edited",
         "bcc_saved": "BCC map entry saved",
         "bcc_saved": "BCC map entry saved",
+        "cors_headers_edited": "CORS headers successfully set.",
         "db_init_complete": "Database initialization completed",
         "db_init_complete": "Database initialization completed",
         "delete_filter": "Deleted filters ID %s",
         "delete_filter": "Deleted filters ID %s",
         "delete_filters": "Deleted filters: %s",
         "delete_filters": "Deleted filters: %s",

+ 33 - 0
data/web/templates/admin/tab-config-admins.twig

@@ -97,6 +97,39 @@
           <div class="col-lg-12">
           <div class="col-lg-12">
             <p class="text-muted">{{ lang.admin.api_info|raw }}</p>
             <p class="text-muted">{{ lang.admin.api_info|raw }}</p>
           </div>
           </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="col-lg-6">
             <div class="card mb-3">
             <div class="card mb-3">
               <div class="card-header">
               <div class="card-header">