Y
Please enter a valid 'to' domain or IP address.
@@ -261,7 +279,9 @@
Y
N
-
+
+ Y
+
@@ -278,7 +298,15 @@
Please enter valid IP address(es) or network(s), separated by commas.
-
+
+ 100
+ 599
+ Please enter a valid HTTP response code between 100 and 599
+
+
+
+ Y
+
@@ -289,8 +317,36 @@
Y
-
+
+ Y
+
+
+
+ header_up
+
+ header_up
+ header_down
+
+ Y
+
+
+ Y
+ /^([^"]{0,1024})$/u
+ The header type must not contain quotation marks (") and must be less than 1024 characters.
+
+
+ /^([^"]{0,1024})$/u
+ The header value must not contain quotation marks (") and must be less than 1024 characters.
+
+
+ /^([^"]{0,1024})$/u
+ The header replacement must not contain quotation marks (") and must be less than 1024 characters.
+
+
+ Y
+
+
diff --git a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt
index 9fcca897b..eac7320b6 100644
--- a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt
+++ b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/general.volt
@@ -28,33 +28,6 @@
$(document).ready(function() {
var data_get_map = {'frm_GeneralSettings':"/api/caddy/General/get"};
mapDataToFormUI(data_get_map).done(function(data){
- // console.log("Fetched data:", data); // Log the fetched data
- var generalSettings = data.frm_GeneralSettings.caddy.general;
-
- // Populate TlsAutoHttps dropdown
- var tlsAutoHttpsSelect = $('#caddy\\.general\\.TlsAutoHttps');
- tlsAutoHttpsSelect.empty(); // Clear existing options
- $.each(generalSettings.TlsAutoHttps, function(key, option) {
- if (key !== "") { // Filter out the unwanted "None" option
- tlsAutoHttpsSelect.append(new Option(option.value, key, false, option.selected === 1));
- }
- });
-
- // Populate TlsDnsProvider dropdown
- var tlsDnsProviderSelect = $('#caddy\\.general\\.TlsDnsProvider');
- tlsDnsProviderSelect.empty(); // Clear existing options
- $.each(generalSettings.TlsDnsProvider, function(key, option) {
- if (key !== "") { // Filter out the unwanted "None" option
- tlsDnsProviderSelect.append(new Option(option.value, key, false, option.selected === 1));
- }
- });
-
- // Populate Trusted Proxies dropdown
- var accesslistSelect = $('#caddy\\.general\\.accesslist');
- accesslistSelect.empty(); // Clear existing options
- $.each(generalSettings.accesslist, function(key, option) {
- accesslistSelect.append(new Option(option.value, key, false, option.selected === 1));
- });
// Refresh selectpicker for these dropdowns
$('.selectpicker').selectpicker('refresh');
diff --git a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt
index 92aaac25d..7f397b615 100644
--- a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt
+++ b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/reverse_proxy.volt
@@ -69,6 +69,14 @@
del:'/api/caddy/ReverseProxy/delBasicAuth/',
});
+ $("#reverseHeaderGrid").UIBootgrid({
+ search:'/api/caddy/ReverseProxy/searchHeader/',
+ get:'/api/caddy/ReverseProxy/getHeader/',
+ set:'/api/caddy/ReverseProxy/setHeader/',
+ add:'/api/caddy/ReverseProxy/addHeader/',
+ del:'/api/caddy/ReverseProxy/delHeader/',
+ });
+
// Function to show alerts in the HTML message area
function showAlert(message, type = "error") {
var alertClass = type === "error" ? "alert-danger" : "alert-success";
@@ -143,6 +151,7 @@
Domains
Handlers
Access
+ Headers
@@ -234,6 +243,7 @@
Subdomain |
Handle Type |
Handle Path |
+ Header |
Upstream Domain |
Upstream Port |
Upstream Path |
@@ -275,6 +285,8 @@
Name |
Client IPs |
Invert |
+ HTTP Code |
+ HTTP Message |
Description |
Commands |
@@ -322,6 +334,39 @@
+
+
+
@@ -351,3 +396,4 @@
{{ partial("layout_partials/base_dialog",['fields':formDialogHandle,'id':'DialogHandle','label':lang._('Edit Handler')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogAccessList,'id':'DialogAccessList','label':lang._('Edit Access List')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogBasicAuth,'id':'DialogBasicAuth','label':lang._('Edit Basic Auth')])}}
+{{ partial("layout_partials/base_dialog",['fields':formDialogHeader,'id':'DialogHeader','label':lang._('Edit Header')])}}
diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_control.py b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_control.py
index da949afef..6598926dc 100755
--- a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_control.py
+++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_control.py
@@ -64,9 +64,10 @@ def run_service_command(action, action_message):
# Updated actions dictionary
actions = {
- "start": "onestart",
- "stop": "onestop",
- "restart": "onerestart",
+ "start": "start",
+ "stop": "stop",
+ "restart": "restart",
+ "reload": "reload",
"validate": "validate" # Validate action
}
diff --git a/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf b/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf
index 28b10be8d..cd5f1b2f6 100644
--- a/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf
+++ b/www/caddy/src/opnsense/service/conf/actions.d/actions_caddy.conf
@@ -14,9 +14,16 @@ message:Stopping Caddy service
command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py restart
parameters:
type:script
-message:Reloading Caddy configuration
+message:Restarting Caddy service
description:Restart Caddy service
+[reload]
+command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py reload
+parameters:
+type:script
+message:Reloading Caddy configuration
+description:Reload Caddy service
+
[validate]
command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_control.py validate
parameters:
diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile
index e0e84bc72..6a4272fb2 100644
--- a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile
+++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/Caddyfile
@@ -1,9 +1,40 @@
+{#
+# Copyright (c) 2023-2024 Cedrik Pischem
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. 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 ``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
+# AUTHOR 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.
+#}
+
# DO NOT EDIT THIS FILE -- OPNsense auto-generated file
{% set generalSettings = helpers.getNodeByTag('Pischem.caddy.general') %}
# Global Options
{
+ {#
+ # Section: Global Log Settings
+ # Purpose: Sets up global log settings. The time format and unix socket make Caddy compatible
+ # with the syslog-ng instance running on the OPNsense.
+ #}
log {
{% if generalSettings.LogAccessPlain|default("0") == "0" %}
{% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %}
@@ -19,6 +50,11 @@
}
}
+ {#
+ # Section: Global Trusted Proxy and Credential Logging
+ # Purpose: The trusted proxy section is important when using CDNs so that headers are trusted.
+ # Credential logging is useful for troubleshooting basic auth.
+ #}
{% set accessListUuid = generalSettings.accesslist %}
{% set logCredentials = generalSettings.LogCredentials %}
@@ -47,6 +83,26 @@
}
{% endif %}
+ {#
+ # Section: Dynamic DNS Global Configuration
+ # Purpose: Sets up global configuration for Dynamic DNS. Caddy needs to be compiled with
+ # https://github.com/mholt/caddy-dynamicdns and https://github.com/caddy-dns. Otherwise the
+ # generated Caddyfile won't run. Each DNS Provider that is added below has to be compiled in.
+ # Some Providers don't support setting A and AAAA-Records, like acmedns.
+ # Most need specific configurations. Since only one provider can be used at the same time,
+ # they all share the same fields for configuration.
+ # Parameters:
+ # - @param dnsProvider (string): Specifies the DNS provider for DDNS updates.
+ # - @param dnsApiKey (string): The API key for authenticating with the DNS provider.
+ # - @param dnsSecretApiKey (string): A secret API key or token for additional authentication security.
+ # - @param dnsOptionalField1 to 4 (string): Optional configuration field for the DNS provider.
+ # - @param dynDnsSimpleHttp (string): URL for a simple HTTP-based service to discover the server's public IP.
+ # - @param dynDnsInterface (string): Network interface(s) to use for IP discovery.
+ # - @param dynDnsCheckInterval (integer): Interval in minutes to check for IP changes.
+ # - @param dynDnsIpVersions (string): The IP version(s) (IPv4, IPv6) for the DDNS update.
+ # - @param dynDnsTTL (integer): Time-To-Live for the DNS records, in hours.
+ # - @param dynDnsDomains (list): Domains and subdomains list for which DDNS updates are enabled.
+ #}
{% set dnsProvider = helpers.toList('Pischem.caddy.general.TlsDnsProvider') | first %}
{% set dnsApiKey = generalSettings.TlsDnsApiKey %}
{% set dnsSecretApiKey = generalSettings.TlsDnsSecretApiKey %}
@@ -78,7 +134,7 @@
{% endfor %}
{% endfor %}
- {% if dnsProvider and dnsProvider != "none" and dnsProvider != "acmedns" and dynDnsDomains|length > 0 %}
+ {% if dnsProvider and dnsProvider != "acmedns" and dynDnsDomains|length > 0 %}
dynamic_dns {
{% if dnsProvider in ['porkbun', 'desec', 'route53', 'googleclouddns', 'azure', 'ovh', 'namecheap', 'powerdns', 'ddnss', 'linode', 'tencentcloud', 'dinahosting', 'hexonet', 'mailinabox'] %}
provider {{ dnsProvider }} {
@@ -207,12 +263,18 @@
}
{% endif %}
+ {#
+ # Section: ACME Email, Auto HTTPS selection and global import statement
+ # Purpose: The ACME email is optional for receiving certificate notices.
+ # Auto HTTPS is optional, the default is on (which means the section is empty).
+ # The import statement is for user specific configuration out of scope of this template.
+ #}
{% set emailValue = helpers.toList('Pischem.caddy.general.TlsEmail') | first %}
{% if emailValue %}
email {{ emailValue }}
{% endif %}
{% set autoHttpsValue = helpers.toList('Pischem.caddy.general.TlsAutoHttps') | first %}
- {% if autoHttpsValue != "on" %}
+ {% if autoHttpsValue %}
auto_https {{ autoHttpsValue }}
{% endif %}
import /usr/local/etc/caddy/caddy.d/*.global
@@ -220,6 +282,11 @@
# Reverse Proxy Configuration
+{#
+# Section: HTTP-01 Challenge Redirection
+# Purpose: A small premade reverse_proxy section
+# that can redirect the HTTP-01 challenge to a different webserver.
+#}
{% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %}
{% if reverse.enabled|default("0") == "1" and reverse.AcmePassthrough %}
# HTTP-01 challenge redirection for domain: "{{ reverse['@uuid'] }}"
@@ -234,8 +301,21 @@
{% endif %}
{% endfor %}
+{#
+# Macro: tls_configuration
+# Purpose: Configures TLS settings based on the DNS provider, API keys, and optional fields.
+# Sets up the Caddyfile to update TXT Records with the chosen DNS Provider and receive
+# certificates with the DNS-01 challenge. Refer to Dynamic DNS section for more details.
+# Parameters:
+# - @param dnsProvider (string): The DNS provider used for the DNS challenge.
+# - @param dnsApiKey (string): API key for the DNS provider, essential for authentication.
+# - @param customCert (string, optional): The config extracted name of a certificate.
+# - @param dnsChallenge (boolean): Indicates if a DNS challenge is used for certificate authentication.
+# - @param dnsSecretApiKey (string, optional): A secret API key or token for additional security, depending on the provider.
+# - @param TlsDnsOptionalField1 to 4 (string, optional): Additional fields for specific DNS provider configurations.
+#}
{% macro tls_configuration(dnsProvider, dnsApiKey, customCert, dnsChallenge, dnsSecretApiKey, TlsDnsOptionalField1, TlsDnsOptionalField2, TlsDnsOptionalField3, TlsDnsOptionalField4) %}
- {% if dnsChallenge == "1" and dnsProvider and dnsProvider != "none" %}
+ {% if dnsChallenge == "1" and dnsProvider %}
{% if dnsProvider in ['duckdns', 'porkbun', 'desec', 'route53', 'acmedns', 'googleclouddns', 'azure', 'ovh', 'namecheap', 'powerdns', 'ddnss', 'linode', 'tencentcloud', 'dinahosting', 'hexonet', 'mailinabox'] %}
tls {
dns {{ dnsProvider }} {
@@ -360,12 +440,74 @@
{% endif %}
{% endmacro %}
+{#
+# Macro: header_manipulation
+# Purpose: Customizes HTTP headers for requests or responses; to add, remove, or modify headers.
+# It uses a 'handle' object that specifies which headers to manipulate based on their @UUIDs.
+# Each handle can have multiple of these HTTP headers assigned.
+# Parameters:
+# @param handle (@object):
+# - @uuid (@string)
+# - HeaderUpDown (string): Determines the direction of the header.
+# - HeaderType (string): Specifies the name of the header.
+# - HeaderValue (string, optional): The new value to set for the header, if any.
+# - HeaderReplace (string, optional): Specifies a value to replace in the header.
+#}
+{% macro header_manipulation(handle) %}
+ {% if handle.header %}
+ {% for header_uuid in handle.header.split(',') %}
+ {% set header = helpers.toList('Pischem.caddy.reverseproxy.header') | selectattr('@uuid', 'equalto', header_uuid) | first %}
+ {# Generate directive only if HeaderUpDown and HeaderType are present #}
+ {% if header.HeaderUpDown and header.HeaderType %}
+ {# Prepare variables, making HeaderValue and HeaderReplace optional #}
+ {% set header_value = header.HeaderValue | default('') %}
+ {% set header_replace = header.HeaderReplace | default('') %}
+ {# Adjust output formatting based on the presence and style of HeaderValue #}
+ {% if header.HeaderReplace and header.HeaderValue %}
+ {% if header_value.startswith('{') %}
+ {{ header.HeaderUpDown }} {{ header.HeaderType }} {{ header_value }} "{{ header_replace }}"
+ {% else %}
+ {{ header.HeaderUpDown }} {{ header.HeaderType }} "{{ header_value }}" "{{ header_replace }}"
+ {% endif %}
+ {% elif header.HeaderValue %}
+ {% if header_value.startswith('{') %}
+ {{ header.HeaderUpDown }} {{ header.HeaderType }} {{ header_value }}
+ {% else %}
+ {{ header.HeaderUpDown }} {{ header.HeaderType }} "{{ header_value }}"
+ {% endif %}
+ {% else %}
+ {{ header.HeaderUpDown }} {{ header.HeaderType }}
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+{% endmacro %}
+
+{#
+# Macro: reverse_proxy_configuration
+# Purpose: Sets up the handle with the reverse proxy configurations. The TLS Settings are generated here for the Upstream.
+# Integrated Macros: header_manipulation
+# Parameters:
+# @param handle (@object):
+# - @uuid (@string)
+# - HandleType (string): Specifies the handling strategy.
+# - HandlePath (string, optional): The path the handle should match on.
+# - ToDomain (string): Target domain for the reverse proxy.
+# - ToPort (string, optional): Target port on the ToDomain.
+# - ToPath (string, optional): Destination path on the ToDomain.
+# - HttpTls (boolean, optional): Enable TLS for the connection.
+# - HttpNtlm (boolean, optional): Enable NTLM authentication for the connection.
+# - HttpTlsInsecureSkipVerify (boolean, optional): If true, the server's SSL certificate is not verified.
+# - HttpTlsTrustedCaCerts (string, optional): The config extracted name of a CA certificate.
+# - HttpTlsServerName (string, optional): Specifies the server name for the TLS handshake.
+#}
{% macro reverse_proxy_configuration(handle) %}
{{ handle.HandleType }} {{ handle.HandlePath|default("") }} {
{% if handle.ToPath|default("") != "" %}
rewrite * {{ handle.ToPath }}{uri}
{% endif %}
reverse_proxy {{ handle.ToDomain }}{% if handle.ToPort %}:{{ handle.ToPort }}{% endif %} {
+ {{ header_manipulation(handle) }}
{% if handle.HttpTls|default("0") == "1" or handle.HttpTlsInsecureSkipVerify|default("0") == "1" %}
{% if handle.HttpNtlm|default("0") == "1" %}
transport http_ntlm {
@@ -401,6 +543,18 @@
}
{% endmacro %}
+{#
+# Macro: access_list_configuration
+# Purpose: Defines access lists based on client IP addresses. The standard logic is "allow these IP addresses, deny all others."
+# A handle with an @ matcher is created that will put the reverse_proxy_configuration inside. That means, the traffic will
+# only get to the reverse proxy, when the access list matches. Invert is also possible, to explicitely deny IPs.
+# The assembly is handled by the "Section: Reverse Proxy Configurations".
+# Parameters:
+# @param accesslist (@object):
+# - @uuid (@string)
+# - clientIps (@string): A comma-separated list of client IP addresses
+# - invert (@boolean): A flag that inverts the logic of the access list
+#}
{% macro access_list_configuration(accesslist, invert) %}
{% set client_ips = accesslist.clientIps.split(',') %}
{% set client_ips_space_separated = client_ips | join(' ') %}
@@ -409,6 +563,16 @@
}
{% endmacro %}
+{#
+# Macro: basicauth_configuration
+# Purpose: Implements basic authentication with a username and password for access.
+# Parameters:
+# @param basicauth_uuids (@string): A comma-separated list of UUIDs, each UUID corresponding to
+# a specific user credentials (username and password).
+# - @uuid (@string)
+# - basicauthuser (@string): The username required for authentication.
+# - basicauthpass (@string): The password associated with the username.
+#}
{% macro basicauth_configuration(basicauth_uuids) %}
{% if basicauth_uuids %}
basicauth {
@@ -422,6 +586,20 @@
{% endif %}
{% endmacro %}
+{#
+# Section: Reverse Proxy Configurations
+# Purpose: Assembles reverse proxy configurations using predefined macros.
+# This is the main logic of the whole template, handle with care.
+# Macros Used:
+# - tls_configuration
+# - basicauth_configuration
+# - access_list_configuration
+# - reverse_proxy_configuration
+# - indirect: header_manipulation
+# Important Details:
+# - Order of Path specific Handles - Prioritizes order of specific path handles over catch-all handles.
+# - Order of Wildcard Domains and Subdomains: Handles for wildcard domains come after all subdomains.
+#}
{% for reverse in helpers.toList('Pischem.caddy.reverseproxy.reverse') %}
{% if reverse.enabled|default("0") == "1" %}
# Reverse Proxy Domain: "{{ reverse['@uuid'] }}"
@@ -491,8 +669,17 @@
{% endif %}
{% endfor %}
{% endif %}
- {% if Pischem.caddy.general.abort|default("0") == "1" %}
- abort
+
+ {% if subdomain.accesslist %}
+ {% if accesslist.HttpResponseCode or accesslist.HttpResponseMessage %}
+ respond {{ '"' + accesslist.HttpResponseMessage|default('') + '"' if accesslist.HttpResponseMessage else '' }} {{ accesslist.HttpResponseCode|default(403) }}
+ {% elif Pischem.caddy.general.abort|default("0") == "1" %}
+ abort
+ {% endif %}
+ {% else %}
+ {% if Pischem.caddy.general.abort|default("0") == "1" %}
+ abort
+ {% endif %}
{% endif %}
}
{% endif %}
@@ -531,8 +718,18 @@
{% endif %}
{% endfor %}
{% endif %}
- {% if Pischem.caddy.general.abort|default("0") == "1" %}
- abort
+
+ {% set accesslist = helpers.toList('Pischem.caddy.reverseproxy.accesslist') | selectattr('@uuid', 'equalto', reverse.accesslist) | first %}
+ {% if accesslist %}
+ {% if accesslist.HttpResponseCode or accesslist.HttpResponseMessage %}
+ respond {{ '"' + accesslist.HttpResponseMessage|default('') + '"' if accesslist.HttpResponseMessage else '' }} {{ accesslist.HttpResponseCode|default(403) }}
+ {% elif Pischem.caddy.general.abort|default("0") == "1" %}
+ abort
+ {% endif %}
+ {% else %}
+ {% if Pischem.caddy.general.abort|default("0") == "1" %}
+ abort
+ {% endif %}
{% endif %}
}
{% endif %}