diff --git a/src/common/core/templates/templates/high.json b/src/common/core/templates/templates/high.json index b32c15327..01b2fff2f 100644 --- a/src/common/core/templates/templates/high.json +++ b/src/common/core/templates/templates/high.json @@ -18,6 +18,12 @@ "LETS_ENCRYPT_DNS_PROVIDER": "", "LETS_ENCRYPT_DNS_PROPAGATION": "default", "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM": "", + "USE_CUSTOM_SSL": "no", + "CUSTOM_SSL_CERT_PRIORITY": "file", + "CUSTOM_SSL_CERT": "", + "CUSTOM_SSL_KEY": "", + "CUSTOM_SSL_CERT_DATA": "", + "CUSTOM_SSL_KEY_DATA": "", "ALLOWED_METHODS": "GET|POST|HEAD", "MAX_CLIENT_SIZE": "10m", "HTTP2": "yes", @@ -81,7 +87,13 @@ "LETS_ENCRYPT_CHALLENGE", "LETS_ENCRYPT_DNS_PROVIDER", "LETS_ENCRYPT_DNS_PROPAGATION", - "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM" + "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM", + "USE_CUSTOM_SSL", + "CUSTOM_SSL_CERT_PRIORITY", + "CUSTOM_SSL_CERT", + "CUSTOM_SSL_KEY", + "CUSTOM_SSL_CERT_DATA", + "CUSTOM_SSL_KEY_DATA" ] }, { diff --git a/src/common/core/templates/templates/low.json b/src/common/core/templates/templates/low.json index 681b38c4a..040b35104 100644 --- a/src/common/core/templates/templates/low.json +++ b/src/common/core/templates/templates/low.json @@ -18,6 +18,12 @@ "LETS_ENCRYPT_DNS_PROVIDER": "", "LETS_ENCRYPT_DNS_PROPAGATION": "default", "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM": "", + "USE_CUSTOM_SSL": "no", + "CUSTOM_SSL_CERT_PRIORITY": "file", + "CUSTOM_SSL_CERT": "", + "CUSTOM_SSL_KEY": "", + "CUSTOM_SSL_CERT_DATA": "", + "CUSTOM_SSL_KEY_DATA": "", "ALLOWED_METHODS": "GET|POST|HEAD|OPTIONS|PUT|DELETE|PATCH", "MAX_CLIENT_SIZE": "100m", "HTTP2": "yes", @@ -79,7 +85,13 @@ "LETS_ENCRYPT_CHALLENGE", "LETS_ENCRYPT_DNS_PROVIDER", "LETS_ENCRYPT_DNS_PROPAGATION", - "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM" + "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM", + "USE_CUSTOM_SSL", + "CUSTOM_SSL_CERT_PRIORITY", + "CUSTOM_SSL_CERT", + "CUSTOM_SSL_KEY", + "CUSTOM_SSL_CERT_DATA", + "CUSTOM_SSL_KEY_DATA" ] }, { diff --git a/src/common/core/templates/templates/medium.json b/src/common/core/templates/templates/medium.json index ccd39d9a6..ea94ac748 100644 --- a/src/common/core/templates/templates/medium.json +++ b/src/common/core/templates/templates/medium.json @@ -18,6 +18,12 @@ "LETS_ENCRYPT_DNS_PROVIDER": "", "LETS_ENCRYPT_DNS_PROPAGATION": "default", "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM": "", + "USE_CUSTOM_SSL": "no", + "CUSTOM_SSL_CERT_PRIORITY": "file", + "CUSTOM_SSL_CERT": "", + "CUSTOM_SSL_KEY": "", + "CUSTOM_SSL_CERT_DATA": "", + "CUSTOM_SSL_KEY_DATA": "", "ALLOWED_METHODS": "GET|POST|HEAD|OPTIONS|PUT|DELETE|PATCH", "MAX_CLIENT_SIZE": "50m", "HTTP2": "yes", @@ -81,7 +87,13 @@ "LETS_ENCRYPT_CHALLENGE", "LETS_ENCRYPT_DNS_PROVIDER", "LETS_ENCRYPT_DNS_PROPAGATION", - "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM" + "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM", + "USE_CUSTOM_SSL", + "CUSTOM_SSL_CERT_PRIORITY", + "CUSTOM_SSL_CERT", + "CUSTOM_SSL_KEY", + "CUSTOM_SSL_CERT_DATA", + "CUSTOM_SSL_KEY_DATA" ] }, { diff --git a/src/common/core/ui/confs/modsec-crs/ui.conf b/src/common/core/ui/confs/modsec-crs/ui.conf index 8ff3197c8..feb293320 100644 --- a/src/common/core/ui/confs/modsec-crs/ui.conf +++ b/src/common/core/ui/confs/modsec-crs/ui.conf @@ -1,6 +1,6 @@ {%- if USE_UI == "yes" -%} -SecRule REQUEST_FILENAME "@rx /(global-config|services/.+)$" "id:1007771,ctl:ruleRemoveById=932235,nolog" +SecRule REQUEST_FILENAME "@rx /(global-config|services/.+)$" "id:1007771,ctl:ruleRemoveById=932235,ctl:ruleRemoveByTag=attack-rfi,nolog" SecRule REQUEST_FILENAME "@rx /(services|cache)/.+$" "id:1007772,ctl:ruleRemoveById=920440,nolog" -SecRule REQUEST_FILENAME "@rx /(configs)/.+$" "id:1007773,ctl:ruleRemoveByTag=attack-rce,nolog" +SecRule REQUEST_FILENAME "@rx /(configs)/.+$" "id:1007773,ctl:ruleRemoveByTag=attack-rce,ctl:ruleRemoveByTag=attack-rfi,nolog" SecRule REQUEST_FILENAME "@endsWith /logs" "id:1007774,ctl:ruleRemoveById=953100,nolog" {%- endif %} diff --git a/src/ui/app/models/config.py b/src/ui/app/models/config.py index 122489ec1..fa0089a4a 100644 --- a/src/ui/app/models/config.py +++ b/src/ui/app/models/config.py @@ -116,7 +116,6 @@ def check_variables( config: dict, *, global_config: bool = False, - ignored_multiples: Optional[Set[str]] = None, new: bool = False, threaded: bool = False, ) -> dict: @@ -197,9 +196,8 @@ def check_variables( flash(message, "error") variables.pop(k) - ignored_multiples = ignored_multiples or set() for k in config: - if k in plugins_settings or k in ignored_multiples: + if k in plugins_settings: continue setting = k[0 : k.rfind("_")] # noqa: E203 diff --git a/src/ui/app/routes/global_config.py b/src/ui/app/routes/global_config.py index 29c285b3a..d4cc0b97f 100644 --- a/src/ui/app/routes/global_config.py +++ b/src/ui/app/routes/global_config.py @@ -32,19 +32,18 @@ def update_global_config(variables: Dict[str, str], threaded: bool = False): wait_applying() # Edit check fields and remove already existing ones - config = DB.get_config(methods=True, with_drafts=True, filtered_settings=list(variables.keys())) + config = DB.get_config(methods=True, with_drafts=True) services = config["SERVER_NAME"]["value"].split(" ") - ignored_multiples = set() for variable, value in variables.copy().items(): setting = config.get(variable, {"value": None, "global": True}) if setting["global"] and value == setting["value"]: if match(r"^.+_\d+$", variable): - ignored_multiples.add(variable) + continue del variables[variable] continue - variables = BW_CONFIG.check_variables(variables, config, global_config=True, ignored_multiples=ignored_multiples, threaded=threaded) + variables = BW_CONFIG.check_variables(variables, config, global_config=True, threaded=threaded) if not variables: content = "The global configuration was not edited because no values were changed." diff --git a/src/ui/app/routes/instances.py b/src/ui/app/routes/instances.py index 714090d4a..cee90ceff 100644 --- a/src/ui/app/routes/instances.py +++ b/src/ui/app/routes/instances.py @@ -69,6 +69,9 @@ def instances_new(): @instances.route("/instances/", methods=["POST"]) @login_required def instances_action(action: Literal["ping", "reload", "stop", "delete"]): # TODO: see if we can support start and restart + if DB.readonly: + return handle_error("Database is in read-only mode", "instances") + verify_data_in_form( data={"instances": None}, err_message=f"Missing instances parameter on /instances/{action}.", diff --git a/src/ui/app/routes/services.py b/src/ui/app/routes/services.py index e1728cd96..3db0a5f1f 100644 --- a/src/ui/app/routes/services.py +++ b/src/ui/app/routes/services.py @@ -204,12 +204,11 @@ def update_service(service: str, variables: Dict[str, str], is_draft: bool, mode if service != "new": db_config = DB.get_config(methods=True, with_drafts=True, service=service) else: - db_config = DB.get_config(global_only=True, methods=True, filtered_settings=list(variables.keys())) + db_config = DB.get_config(global_only=True, methods=True) was_draft = db_config.get("IS_DRAFT", {"value": "no"})["value"] == "yes" old_server_name = variables.pop("OLD_SERVER_NAME", "") - ignored_multiples = set() db_custom_configs = {} new_configs = set() configs_changed = False @@ -279,10 +278,10 @@ def update_service(service: str, variables: Dict[str, str], is_draft: bool, mode for variable, value in variables.copy().items(): if (mode == "advanced" or variable != "SERVER_NAME") and value == db_config.get(variable, {"value": None})["value"]: if match(r"^.+_\d+$", variable): - ignored_multiples.add(variable) + continue del variables[variable] - variables = BW_CONFIG.check_variables(variables, db_config, ignored_multiples=ignored_multiples, new=service == "new", threaded=True) + variables = BW_CONFIG.check_variables(variables, db_config, new=service == "new", threaded=True) if service != "new" and was_draft == is_draft and not variables and not configs_changed: DATA["TO_FLASH"].append( diff --git a/src/ui/app/static/js/plugins-settings.js b/src/ui/app/static/js/plugins-settings.js index 4c9e0e3a8..6041f469a 100644 --- a/src/ui/app/static/js/plugins-settings.js +++ b/src/ui/app/static/js/plugins-settings.js @@ -303,10 +303,17 @@ $(document).ready(() => { settingValue = $this.is(":checked") ? "yes" : "no"; } + // Check if it's a multiple setting with numeric suffix + const isMultipleSetting = + settingName && + $this.attr("id").startsWith("multiple-") && + /_\d+$/.test(settingName); + if ( !isEasy && settingName !== "SERVER_NAME" && - settingValue == originalValue + settingValue == originalValue && + !isMultipleSetting ) return; @@ -1117,7 +1124,7 @@ $(document).ready(() => { }, 30); }); - $(".plugin-setting").on("keydown", function (e) { + $(document).on("keydown", ".plugin-setting", function () { if (e.key === "Enter") { e.preventDefault(); $(".save-settings").trigger("click"); diff --git a/src/ui/app/templates/models/plugins_settings_raw.html b/src/ui/app/templates/models/plugins_settings_raw.html index c98621a35..a25c0a630 100644 --- a/src/ui/app/templates/models/plugins_settings_raw.html +++ b/src/ui/app/templates/models/plugins_settings_raw.html @@ -39,7 +39,7 @@ {% for plugin_data in plugins.values() %} {% set filtered_settings = get_filtered_settings(plugin_data["settings"], current_endpoint == "global-config") %} {% if filtered_settings %} - {% for setting, setting_data in filtered_settings.items() if setting not in blacklisted_settings %} + {% for setting, setting_data in filtered_settings.items() if not setting_data.get('multiple', false) and setting not in blacklisted_settings %} {% set setting_config = config.get(setting, {}) %} {% set setting_default = setting_data.get("default", "") %} {% set setting_value = setting_config.get("value", setting_default) %} @@ -49,6 +49,22 @@ {% endif %} {% endfor %} {% endif %} + {% set plugin_multiples = get_multiples(filtered_settings, config) %} + {% if plugin_multiples %} + {% for multiple, multiples in plugin_multiples.items() %} + {% for setting_suffix, settings in multiples.items() %} + {% for setting, setting_data in settings.items() if setting not in blacklisted_settings %} + {% set setting_config = config.get(setting, {}) %} + {% set setting_default = setting_data.get("default", "") %} + {% set setting_value = setting_config.get("value", setting_default) %} + {% if setting_value != setting_default %} + {% if config_lines.append(setting + "=" + setting_value) %}{% endif %} + {% if default_settings.append(setting + "=" + setting_default) %}{% endif %} + {% endif %} + {% endfor %} + {% endfor %} + {% endfor %} + {% endif %} {% endfor %} {% set raw_config = config_lines | join('\r\n') %} diff --git a/src/ui/app/utils.py b/src/ui/app/utils.py index fa9559a0f..fcdeb946c 100644 --- a/src/ui/app/utils.py +++ b/src/ui/app/utils.py @@ -102,9 +102,11 @@ def handle_stop(signum, frame): def get_multiples(settings: dict, config: dict) -> Dict[str, Dict[str, Dict[str, dict]]]: plugin_multiples = {} + for setting, data in settings.items(): multiple = data.get("multiple") if multiple: + # Add the setting without suffix for reference data = data | {"setting_no_suffix": setting} if multiple not in plugin_multiples: @@ -112,22 +114,41 @@ def get_multiples(settings: dict, config: dict) -> Dict[str, Dict[str, Dict[str, if "0" not in plugin_multiples[multiple]: plugin_multiples[multiple]["0"] = {} + # Add the base (suffix "0") setting plugin_multiples[multiple]["0"].update({setting: data}) - for config_setting in config: + # Process config settings with suffixes + for config_setting, value in config.items(): setting_match = match(setting + r"_(?P\d+)$", config_setting) if setting_match: suffix = setting_match.group("suffix") - if suffix == "0": - continue - if suffix not in plugin_multiples[multiple]: plugin_multiples[multiple][suffix] = {} - plugin_multiples[multiple][suffix].update({config_setting: data}) + plugin_multiples[multiple][suffix][config_setting] = { + **data, + "value": value, # Include the value from the config + } + + # Ensure every suffix group has all settings in the same order as "0" + base_settings = plugin_multiples[multiple]["0"] + for suffix, settings_dict in plugin_multiples[multiple].items(): + if suffix == "0": + continue + for default_setting, default_data in base_settings.items(): + if f"{default_setting}_{suffix}" not in settings_dict: + settings_dict[f"{default_setting}_{suffix}"] = { + **default_data, + "value": default_data.get("value"), # Default value if not in config + } + + # Preserve the order of settings based on suffix "0" + plugin_multiples[multiple][suffix] = { + f"{default_setting}_{suffix}": settings_dict[f"{default_setting}_{suffix}"] for default_setting in base_settings + } # Sort the multiples and their settings for multiple, multiples in plugin_multiples.items(): - plugin_multiples[multiple] = dict(sorted(multiples.items())) + plugin_multiples[multiple] = dict(sorted(multiples.items(), key=lambda x: int(x[0]))) return plugin_multiples