mirror of
https://github.com/opnsense/plugins.git
synced 2026-06-09 08:56:23 -04:00
www/caddy: Cleanup volt templates (#4634)
- Cleanup of styles, formatters, and dialog layouts - Dialog element hiding logic was improved - "Filter by Domain" can filter subdomains - "Filter by Domain" will preselect domain and subdomain in handler add dialog - Grids are now responsive with automatic overflow on small screens - Grids now lazy load on tab switch for improved performance - Grids format domains and upstreams with protocol, domain, port and path in a single line - "Layer4 Proxy" and "Reverse Proxy" now use the same volt template - "Search Handler" and "Add Handler" buttons added to domain and subdomain grids - Step buttons (Add Domain, Add Handler) have been removed since they are redundant - Success messages on configuration apply have been removed, only errors will be shown - Tabs use URL hash to preserve selection on refresh
This commit is contained in:
parent
50d71f192a
commit
b850e22ee8
11 changed files with 602 additions and 571 deletions
|
|
@ -18,6 +18,19 @@ Plugin Changelog
|
|||
* Add: basic_auth per handler (opnsense/plugins/issues/4619)
|
||||
* Change: the ACL has been changed to "page-caddy" in "System: Access: Privileges". Privilege must be reassigned if used. (opnsense/plugins/issues/4623)
|
||||
* Change: standalone certificate widget has been removed, use system default certificate widget instead. (opnsense/plugins/pull/4637)
|
||||
* Cleanup: UI improvements (opnsense/plugins/pull/4634)
|
||||
* Cleanup of styles, formatters, and dialog layouts
|
||||
* Dialog element hiding logic was improved
|
||||
* "Filter by Domain" can filter subdomains
|
||||
* "Filter by Domain" will preselect domain and subdomain in handler add dialog
|
||||
* Grids are now responsive with automatic overflow on small screens
|
||||
* Grids now lazy load on tab switch for improved performance
|
||||
* Grids format domains and upstreams with protocol, domain, port and path in a single line
|
||||
* "Layer4 Proxy" and "Reverse Proxy" now use the same volt template
|
||||
* "Search Handler" and "Add Handler" buttons added to domain and subdomain grids
|
||||
* Step buttons (Add Domain, Add Handler) have been removed since they are redundant
|
||||
* Success messages on configuration apply have been removed, only errors will be shown
|
||||
* Tabs use URL hash to preserve selection on refresh
|
||||
|
||||
1.8.4
|
||||
|
||||
|
|
|
|||
|
|
@ -38,77 +38,94 @@ class ReverseProxyController extends ApiMutableModelControllerBase
|
|||
protected static $internalModelUseSafeDelete = true;
|
||||
|
||||
/**
|
||||
* Function for search filter dropdown
|
||||
*
|
||||
* @return array containing rows of domain and port combinations.
|
||||
* Return selectpicker options for reverse proxy domains and ports
|
||||
*/
|
||||
public function getAllReverseDomainsAction()
|
||||
{
|
||||
$result = array("rows" => array());
|
||||
$result = [
|
||||
'domains' => [
|
||||
'label' => gettext('Domains'),
|
||||
'icon' => 'fa fa-fw fa-globe text-success',
|
||||
'items' => []
|
||||
],
|
||||
'subdomains' => [
|
||||
'label' => gettext('Subdomains'),
|
||||
'icon' => 'fa fa-fw fa-globe text-warning',
|
||||
'items' => []
|
||||
],
|
||||
];
|
||||
|
||||
$mdlCaddy = new \OPNsense\Caddy\Caddy();
|
||||
$reverseNodes = $mdlCaddy->reverseproxy->reverse->iterateItems();
|
||||
foreach ((new \OPNsense\Caddy\Caddy())->reverseproxy->reverse->iterateItems() as $reverse) {
|
||||
if (!empty($reverse->FromDomain)) {
|
||||
$port = (string)$reverse->FromPort;
|
||||
$combined = (string)$reverse->FromDomain . ($port !== '' ? ':' . $port : '');
|
||||
|
||||
foreach ($reverseNodes as $item) {
|
||||
if (!empty($item->FromDomain)) {
|
||||
// Conditionally concatenate port if it exists
|
||||
$domain = (string)$item->FromDomain;
|
||||
$port = (string)$item->FromPort;
|
||||
$combinedDomainPort = $domain . (!empty($port) ? ':' . $port : '');
|
||||
|
||||
$result['rows'][] = array(
|
||||
'id' => (string)$item->getAttributes()['uuid'],
|
||||
'domainPort' => $combinedDomainPort // Combined domain and port, conditionally adding port
|
||||
);
|
||||
$result['domains']['items'][] = [
|
||||
'value' => (string)$reverse->getAttributes()['uuid'],
|
||||
'label' => $combined
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ((new \OPNsense\Caddy\Caddy())->reverseproxy->subdomain->iterateItems() as $subdomain) {
|
||||
if (!empty($subdomain->FromDomain)) {
|
||||
|
||||
$result['subdomains']['items'][] = [
|
||||
'value' => (string)$subdomain->getAttributes()['uuid'],
|
||||
'label' => (string)$subdomain->FromDomain
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_keys($result) as $key) {
|
||||
usort($result[$key]['items'], fn($a, $b) => strcasecmp($a['label'], $b['label']));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generalized helper function for searching across different sections of the reverse proxy setup.
|
||||
* This function mostly helps when model relation fields are used.
|
||||
* It filters entries based on UUIDs provided as an argument. The section or key used for the UUID
|
||||
* can be specified, allowing for direct or indirect UUID referencing.
|
||||
* Build a UUID filter function.
|
||||
*
|
||||
* @param string $modelPath The data model path identifier, pointing to section of model being searched.
|
||||
* @param string $uuidSearchBase The request parameter name for the comma-separated list of UUIDs.
|
||||
* @param string|null $uuidReferenceKey Attribute key used to fetch the UUID for filtering.
|
||||
* If null, uses item's own UUID.
|
||||
* @return array Filtered search results.
|
||||
* @return callable|null
|
||||
*/
|
||||
private function searchActionHelper($modelPath, $uuidSearchBase, $uuidReferenceKey = null)
|
||||
private function buildFilterFunction(): ?callable
|
||||
{
|
||||
// Fetch the comma-separated UUIDs string from the request using the provided parameter name.
|
||||
$uuidList = $this->request->get($uuidSearchBase);
|
||||
// Ensure the retrieved UUID list is a string and not empty before attempting to explode it.
|
||||
$uuidArray = (!empty($uuidList) && is_string($uuidList)) ? explode(',', $uuidList) : [];
|
||||
$domainUuids = $this->request->get('domainUuids');
|
||||
if (empty($domainUuids)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Define a filter function to determine which items to include based on the UUID.
|
||||
$filterFunction = function ($modelItem) use ($uuidArray, $uuidReferenceKey) {
|
||||
// Extract UUID from the item, using the specified UUID key if provided, else default to direct UUID access.
|
||||
if ($uuidReferenceKey !== null) {
|
||||
$modelUUID = (string)$modelItem->$uuidReferenceKey;
|
||||
} else {
|
||||
$modelUUID = (string)$modelItem->getAttributes()['uuid'];
|
||||
return function ($record) use ($domainUuids): bool {
|
||||
$fieldsToCheck = ['reverse', 'subdomain'];
|
||||
$uuids = [];
|
||||
|
||||
// Add the record's own UUID
|
||||
$uuidAttr = $record->getAttributes()['uuid'] ?? null;
|
||||
if (is_scalar($uuidAttr)) {
|
||||
$uuids[] = trim((string)$uuidAttr);
|
||||
}
|
||||
// Include the item if the UUID array is empty or if the item's UUID is in the array.
|
||||
return empty($uuidArray) || in_array($modelUUID, $uuidArray, true);
|
||||
|
||||
// Add UUIDs from model relation fields
|
||||
foreach ($fieldsToCheck as $field) {
|
||||
$value = $record->{$field} ?? null;
|
||||
|
||||
foreach ((array)$value as $item) {
|
||||
if (is_scalar($item)) {
|
||||
$uuids[] = trim((string)$item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (bool) array_intersect($uuids, $domainUuids);
|
||||
};
|
||||
|
||||
// Perform the search using the specified model path and the filter function, returning the results.
|
||||
// Note: This uses the existing search function of the ApiMutableModelControllerBase
|
||||
return $this->searchBase($modelPath, null, 'description', $filterFunction);
|
||||
}
|
||||
|
||||
|
||||
// ReverseProxy Section
|
||||
|
||||
public function searchReverseProxyAction()
|
||||
{
|
||||
// For domains, use their domain UUIDs directly, $uuidReferenceKey null added for explicit clarity
|
||||
return $this->searchActionHelper("reverseproxy.reverse", "reverseUuids", null);
|
||||
return $this->searchBase('reverseproxy.reverse', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function setReverseProxyAction($uuid)
|
||||
|
|
@ -141,9 +158,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase
|
|||
|
||||
public function searchSubdomainAction()
|
||||
{
|
||||
// For subdomains, compare 'reverseUuids' (which contain domain UUIDs)
|
||||
// to 'reverse' (which contain the same domain UUIDs due to model relation field)
|
||||
return $this->searchActionHelper("reverseproxy.subdomain", "reverseUuids", "reverse");
|
||||
return $this->searchBase('reverseproxy.subdomain', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function setSubdomainAction($uuid)
|
||||
|
|
@ -174,12 +189,9 @@ class ReverseProxyController extends ApiMutableModelControllerBase
|
|||
|
||||
// Handler Section
|
||||
|
||||
// Adjusted for search filter dropdown, using helper function
|
||||
public function searchHandleAction()
|
||||
{
|
||||
// For handles, compare 'reverseUuids' (which contain domain UUIDs)
|
||||
// to 'reverse' (which contain the same domain UUIDs due to model relation field)
|
||||
return $this->searchActionHelper("reverseproxy.handle", "reverseUuids", "reverse");
|
||||
return $this->searchBase('reverseproxy.handle', null, null, $this->buildFilterFunction());
|
||||
}
|
||||
|
||||
public function setHandleAction($uuid)
|
||||
|
|
@ -211,7 +223,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase
|
|||
|
||||
public function searchAccessListAction()
|
||||
{
|
||||
return $this->searchBase("reverseproxy.accesslist", null, 'description');
|
||||
return $this->searchBase("reverseproxy.accesslist");
|
||||
}
|
||||
|
||||
public function setAccessListAction($uuid)
|
||||
|
|
@ -239,7 +251,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase
|
|||
|
||||
public function searchBasicAuthAction()
|
||||
{
|
||||
return $this->searchBase("reverseproxy.basicauth", null, 'description');
|
||||
return $this->searchBase("reverseproxy.basicauth");
|
||||
}
|
||||
|
||||
public function setBasicAuthAction($uuid)
|
||||
|
|
@ -291,7 +303,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase
|
|||
|
||||
public function searchHeaderAction()
|
||||
{
|
||||
return $this->searchBase("reverseproxy.header", null, 'description');
|
||||
return $this->searchBase("reverseproxy.header");
|
||||
}
|
||||
|
||||
public function setHeaderAction($uuid)
|
||||
|
|
@ -319,7 +331,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase
|
|||
|
||||
public function searchLayer4Action()
|
||||
{
|
||||
return $this->searchBase("reverseproxy.layer4", null, 'description');
|
||||
return $this->searchBase("reverseproxy.layer4");
|
||||
}
|
||||
|
||||
public function setLayer4Action($uuid)
|
||||
|
|
@ -352,7 +364,7 @@ class ReverseProxyController extends ApiMutableModelControllerBase
|
|||
|
||||
public function searchLayer4OpenvpnAction()
|
||||
{
|
||||
return $this->searchBase("reverseproxy.layer4openvpn", null, 'description');
|
||||
return $this->searchBase("reverseproxy.layer4openvpn");
|
||||
}
|
||||
|
||||
public function setLayer4OpenvpnAction($uuid)
|
||||
|
|
|
|||
|
|
@ -36,12 +36,13 @@ class Layer4Controller extends IndexController
|
|||
{
|
||||
public function indexAction()
|
||||
{
|
||||
$this->view->pick('OPNsense/Caddy/layer4');
|
||||
$this->view->entrypoint = 'layer4';
|
||||
$this->view->pick('OPNsense/Caddy/reverse_proxy');
|
||||
|
||||
$this->view->formDialogLayer4 = $this->getForm('dialogLayer4');
|
||||
$this->view->formGridLayer4 = $this->getFormGrid('dialogLayer4', null, 'ConfChangeMessage');
|
||||
$this->view->formGridLayer4 = $this->getFormGrid('dialogLayer4', 'Layer4');
|
||||
|
||||
$this->view->formDialogLayer4Openvpn = $this->getForm('dialogLayer4Openvpn');
|
||||
$this->view->formGridLayer4Openvpn = $this->getFormGrid('dialogLayer4Openvpn', null, 'ConfChangeMessage');
|
||||
$this->view->formGridLayer4Openvpn = $this->getFormGrid('dialogLayer4Openvpn', 'Layer4Openvpn');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,24 +36,25 @@ class ReverseProxyController extends IndexController
|
|||
{
|
||||
public function indexAction()
|
||||
{
|
||||
$this->view->entrypoint = 'reverse_proxy';
|
||||
$this->view->pick('OPNsense/Caddy/reverse_proxy');
|
||||
|
||||
$this->view->formDialogReverseProxy = $this->getForm("dialogReverseProxy");
|
||||
$this->view->formGridReverseProxy = $this->getFormGrid('dialogReverseProxy', null, 'ConfChangeMessage');
|
||||
$this->view->formGridReverseProxy = $this->getFormGrid('dialogReverseProxy', 'ReverseProxy');
|
||||
|
||||
$this->view->formDialogSubdomain = $this->getForm("dialogSubdomain");
|
||||
$this->view->formGridSubdomain = $this->getFormGrid('dialogSubdomain', null, 'ConfChangeMessage');
|
||||
$this->view->formGridSubdomain = $this->getFormGrid('dialogSubdomain', 'Subdomain');
|
||||
|
||||
$this->view->formDialogHandle = $this->getForm("dialogHandle");
|
||||
$this->view->formGridHandle = $this->getFormGrid('dialogHandle', null, 'ConfChangeMessage');
|
||||
$this->view->formGridHandle = $this->getFormGrid('dialogHandle', 'Handle');
|
||||
|
||||
$this->view->formDialogAccessList = $this->getForm("dialogAccessList");
|
||||
$this->view->formGridAccessList = $this->getFormGrid('dialogAccessList', null, 'ConfChangeMessage');
|
||||
$this->view->formGridAccessList = $this->getFormGrid('dialogAccessList', 'AccessList');
|
||||
|
||||
$this->view->formDialogBasicAuth = $this->getForm("dialogBasicAuth");
|
||||
$this->view->formGridBasicAuth = $this->getFormGrid('dialogBasicAuth', null, 'ConfChangeMessage');
|
||||
$this->view->formGridBasicAuth = $this->getFormGrid('dialogBasicAuth', 'BasicAuth');
|
||||
|
||||
$this->view->formDialogHeader = $this->getForm("dialogHeader");
|
||||
$this->view->formGridHeader = $this->getFormGrid('dialogHeader', null, 'ConfChangeMessage');
|
||||
$this->view->formGridHeader = $this->getFormGrid('dialogHeader', 'Header');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,11 @@
|
|||
<type>checkbox</type>
|
||||
<help><![CDATA[Enable this handler.]]></help>
|
||||
<grid_view>
|
||||
<width>6em</width>
|
||||
<width>2em</width>
|
||||
<type>boolean</type>
|
||||
<formatter>rowtoggle</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>handle.description</id>
|
||||
<label>Description</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Enter a description for this handler.]]></help>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Frontend</label>
|
||||
|
|
@ -25,17 +19,22 @@
|
|||
<label>Domain</label>
|
||||
<type>dropdown</type>
|
||||
<help><![CDATA[Select a domain to handle.]]></help>
|
||||
<grid_view>
|
||||
<formatter>model_relation_domain</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>handle.subdomain</id>
|
||||
<label>Subdomain</label>
|
||||
<type>dropdown</type>
|
||||
<help><![CDATA[Select a subdomain to handle. Make sure to additionaly choose a wildcard domain as "Domain". Leave unset, if not using subdomains.]]></help>
|
||||
<grid_view>
|
||||
<formatter>model_relation_domain</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Handler</label>
|
||||
<advanced>true</advanced>
|
||||
</field>
|
||||
<field>
|
||||
<id>handle.HandleType</id>
|
||||
|
|
@ -53,7 +52,6 @@
|
|||
<type>text</type>
|
||||
<hint>any</hint>
|
||||
<help><![CDATA[Enter a path to handle. Choose a pattern like "/*" or "/example/*". Leave empty to handle any paths (recommended). Any request matching this pattern will be handled by the chosen directive.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -61,14 +59,15 @@
|
|||
<field>
|
||||
<type>header</type>
|
||||
<label>Access</label>
|
||||
<advanced>true</advanced>
|
||||
<collapse>true</collapse>
|
||||
<style>style_reverse_proxy</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>handle.accesslist</id>
|
||||
<label>Access List</label>
|
||||
<type>dropdown</type>
|
||||
<style>style_reverse_proxy</style>
|
||||
<help><![CDATA[Select an Access List to restrict access to this handler. If unset, any local or remote client is allowed access.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -78,6 +77,7 @@
|
|||
<label>Basic Auth</label>
|
||||
<type>select_multiple</type>
|
||||
<size>5</size>
|
||||
<style>selectpicker style_reverse_proxy</style>
|
||||
<help><![CDATA[Select Users to restrict access to this path. If unset, any user is allowed access.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
|
|
@ -87,8 +87,8 @@
|
|||
<id>handle.ForwardAuth</id>
|
||||
<label>Forward Auth</label>
|
||||
<type>checkbox</type>
|
||||
<style>style_reverse_proxy</style>
|
||||
<help><![CDATA[Enable or disable Forward Auth. Requires an "Auth Provider" in "General Settings". Headers are set automatically to the standard of the chosen provider. Enabling this option will additionally generate the forward_auth directive in front of the reverse_proxy directive inside the scope of this handler.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<type>boolean</type>
|
||||
|
|
@ -107,7 +107,9 @@
|
|||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Upstream</label>
|
||||
<label>Transport</label>
|
||||
<collapse>true</collapse>
|
||||
<style>style_reverse_proxy</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>handle.HttpVersion</id>
|
||||
|
|
@ -115,7 +117,6 @@
|
|||
<type>dropdown</type>
|
||||
<style>selectpicker style_reverse_proxy</style>
|
||||
<help><![CDATA[The default versions are highly recommended. Choose a HTTP version for the upstream destination. HTTP/3 (HTTP over QUIC) requires HTTPS, and only establishes connections to webservers that also support HTTP/3.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -128,7 +129,6 @@
|
|||
<size>5</size>
|
||||
<style>selectpicker style_reverse_proxy</style>
|
||||
<help><![CDATA[Select one or multiple headers. Caddy sets "X-Forwarded-For", "X-Forwarded-Proto" and "X-Forwarded-Host" by default, adding them here is not needed. Setting a wrong configuration can be a security risk or break functionality.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -140,16 +140,22 @@
|
|||
<hint>120</hint>
|
||||
<style>style_reverse_proxy</style>
|
||||
<help><![CDATA[Leave empty to use default. Keepalive is either 0 (off) or a duration value that specifies how long to keep connections open (timeout) in seconds.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Upstream</label>
|
||||
</field>
|
||||
<field>
|
||||
<id>handle.HttpTls</id>
|
||||
<label>Protocol</label>
|
||||
<type>dropdown</type>
|
||||
<help><![CDATA[Enable or disable HTTP over TLS (HTTPS) to communicate with the upstream destination. Caddy uses HTTP with the upstream destination by default.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>handle.ToDomain</id>
|
||||
|
|
@ -158,19 +164,24 @@
|
|||
<style>tokenize</style>
|
||||
<allownew>true</allownew>
|
||||
<help><![CDATA[Enter a domain name or IP address of the upstream destination. If multiple are chosen, they will be load balanced with the default random policy. A health check can be activated by populating "Upstream Fail Duration" in advanced mode. When directive is "redir", only the first domain will be evaluated.]]></help>
|
||||
<grid_view>
|
||||
<formatter>to_domain</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>handle.ToPort</id>
|
||||
<label>Upstream Port</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Leave empty to use the default port or choose a custom port for the upstream destination.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>handle.ToPath</id>
|
||||
<label>Upstream Path</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[When directive is "reverse_proxy", enter a path prefix like "/guacamole" that should be prepended to the upstream request. This is useful for destinations that have a virtual directory as their base path. When directive is "redir", add the path the request should be redirected to; leaving it empty will append {uri}.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -179,7 +190,7 @@
|
|||
<id>handle.HttpTlsInsecureSkipVerify</id>
|
||||
<label>TLS Insecure Skip Verify</label>
|
||||
<type>checkbox</type>
|
||||
<style>style_tls style_reverse_proxy</style>
|
||||
<style>style_tls_handle</style>
|
||||
<help><![CDATA[Caddy uses HTTP by default to connect to the Upstream. If the Upstream is only reachable via HTTPS, this option disables the TLS handshake verification. This makes the connection insecure and vulnerable to man-in-the-middle attacks. In private networks the risk is low, though do not use in production if possible. It is advised to either use plain HTTP, or proper TLS handling by using the options in "Trust".]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
|
|
@ -191,7 +202,7 @@
|
|||
<id>handle.HttpTlsTrustedCaCerts</id>
|
||||
<label>TLS Trust Pool</label>
|
||||
<type>dropdown</type>
|
||||
<style>selectpicker style_tls style_reverse_proxy</style>
|
||||
<style>selectpicker style_tls_handle</style>
|
||||
<help><![CDATA[Choose a CA or self-signed certificate to trust from "System - Trust - Authorities". Useful if the upstream destination only accepts TLS connections and offers a self signed certificate. Adding that certificate here will allow for the encrypted connection to succeed.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
|
|
@ -201,7 +212,7 @@
|
|||
<id>handle.HttpTlsServerName</id>
|
||||
<label>TLS Server Name</label>
|
||||
<type>text</type>
|
||||
<style>style_tls style_reverse_proxy</style>
|
||||
<style>style_tls_handle</style>
|
||||
<help><![CDATA[Enter a hostname or IP address that matches the SAN "Subject Alternative Name" of the offered upstream certificate. This will change the SNI "Server Name Indication" of Caddy. Setting an IP address as "Upstream Domain", enabling "TLS" and selecting a "TLS Trust Pool", would make the SAN of the offered upstream certificate not match with the SNI of Caddy, since it will be an IP address instead of a hostname. Setting the hostname of the certificate here, fixes this issue. Please note that only SAN certificates are supported; CN "Common Name" will not work.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
|
|
@ -211,7 +222,7 @@
|
|||
<id>handle.HttpNtlm</id>
|
||||
<label>NTLM</label>
|
||||
<type>checkbox</type>
|
||||
<style>style_tls style_reverse_proxy</style>
|
||||
<style>style_tls_handle</style>
|
||||
<help><![CDATA[Enable or disable NTLM. Needed to reverse proxy an Exchange Server. Warning: NTLM has been deprecated by Microsoft. This option will stay for as long as the optional http.reverse_proxy.transport.http_ntlm module can be compiled without errors.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
|
|
@ -219,10 +230,17 @@
|
|||
<formatter>boolean</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>handle.description</id>
|
||||
<label>Description</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Enter a description for this handler.]]></help>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Load Balancing</label>
|
||||
<advanced>true</advanced>
|
||||
<collapse>true</collapse>
|
||||
<style>style_reverse_proxy</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>handle.lb_policy</id>
|
||||
|
|
@ -230,7 +248,6 @@
|
|||
<type>dropdown</type>
|
||||
<style>selectpicker style_reverse_proxy</style>
|
||||
<help><![CDATA[lb_policy is the name of the load balancing policy, along with any options. For policies that involve hashing, the highest-random-weight (HRW) algorithm is used to ensure that a client or request with the same hash key is mapped to the same upstream, even if the list of upstreams change.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -242,7 +259,6 @@
|
|||
<style>style_reverse_proxy</style>
|
||||
<hint>off</hint>
|
||||
<help><![CDATA[lb_retries is how many times to retry selecting available backends for each request if the next available host is down. If lb_try_duration is also configured, then retries may stop early if the duration is reached. In other words, the retry duration takes precedence over the retry count.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -254,7 +270,6 @@
|
|||
<style>style_reverse_proxy</style>
|
||||
<hint>0</hint>
|
||||
<help><![CDATA[lb_try_duration is a duration value that defines how long to try selecting available backends for each request if the next available host is down. Clients will wait for up to this long while the load balancer tries to find an available upstream host. A reasonable starting point might be 5s since the HTTP transport's default dial timeout is 3s, so that should allow for at least one retry if the first selected upstream cannot be reached.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -266,7 +281,6 @@
|
|||
<style>style_reverse_proxy</style>
|
||||
<hint>250</hint>
|
||||
<help><![CDATA[lb_try_interval is a duration value that defines how long to wait between selecting the next host from the pool. Only relevant when a request to an upstream host fails. Be aware that setting this to 0 with a non-zero lb_try_duration can cause the CPU to spin if all backends are down and latency is very low.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -278,7 +292,6 @@
|
|||
<style>style_reverse_proxy</style>
|
||||
<hint>off</hint>
|
||||
<help><![CDATA[fail_duration enables a passive health check when multiple destinations in "Upstream Domain" are set. It is a value that defines how long to remember a failed request. A duration of 1 or more seconds enables passive health checking. A reasonable starting point might be 30s to balance error rates with responsiveness when bringing an unhealthy upstream back online.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -290,7 +303,6 @@
|
|||
<style>style_reverse_proxy</style>
|
||||
<hint>1</hint>
|
||||
<help><![CDATA[max_fails is the maximum number of failed requests within fail_duration that are needed before considering an upstream to be down.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -302,7 +314,6 @@
|
|||
<style>style_reverse_proxy</style>
|
||||
<hint>off</hint>
|
||||
<help><![CDATA[unhealthy_status counts a request as failed if the response comes back with one of these status codes. Can be a 3-digit status code or a status code class ending in xx, for example: 404 or 5xx.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -314,7 +325,6 @@
|
|||
<style>style_reverse_proxy</style>
|
||||
<hint>off</hint>
|
||||
<help><![CDATA[unhealthy_latency is a duration value in ms that counts a request as failed if it takes this long to get a response.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -326,7 +336,6 @@
|
|||
<style>style_reverse_proxy</style>
|
||||
<hint>off</hint>
|
||||
<help><![CDATA[unhealthy_request_count is the permissible number of simultaneous requests to a backend before marking it as down. In other words, if a particular backend is currently handling this many requests, then it is considered "overloaded" and other backends will be preferred instead. This should be a reasonably large number; configuring this means that the proxy will have a limit of unhealthy_request_count × upstreams_count total simultaneous requests, and any requests after that point will result in an error due to no upstreams being available.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<type>checkbox</type>
|
||||
<help><![CDATA[Enable this Layer4 route.]]></help>
|
||||
<grid_view>
|
||||
<width>6em</width>
|
||||
<width>2em</width>
|
||||
<type>boolean</type>
|
||||
<formatter>rowtoggle</formatter>
|
||||
</grid_view>
|
||||
|
|
@ -15,12 +15,9 @@
|
|||
<label>Sequence</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Rules are sorted based on the sequence number, higher number means lower priority. Rules without a sequence number will be processed first.]]></help>
|
||||
</field>
|
||||
<field>
|
||||
<id>layer4.description</id>
|
||||
<label>Description</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Enter a description for this Layer4 route.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
|
|
@ -41,6 +38,9 @@
|
|||
<type>dropdown</type>
|
||||
<style>selectpicker style_type</style>
|
||||
<help><![CDATA[Match the received traffic on OSI Layer 4, either TCP or UDP. When "Routing Type" is "listener_wrappers", currently only TCP will match.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>layer4.FromPort</id>
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
<id>layer4.FromDomain</id>
|
||||
<label>Domain</label>
|
||||
<type>select_multiple</type>
|
||||
<style>style_matchers tokenize matchers_domain</style>
|
||||
<style>tokenize style_domain</style>
|
||||
<allownew>true</allownew>
|
||||
<help><![CDATA[Enter one or multiple domains to route via SNI or Host Header. Wildcard domains and host wildcards are allowed, e.g. "*.example.com" and "*".]]></help>
|
||||
</field>
|
||||
|
|
@ -74,7 +74,7 @@
|
|||
<id>layer4.FromOpenvpnModes</id>
|
||||
<label>OpenVPN Mode</label>
|
||||
<type>dropdown</type>
|
||||
<style>selectpicker style_matchers matchers_openvpn</style>
|
||||
<style>selectpicker style_openvpn</style>
|
||||
<help><![CDATA[Select the mode matching the OpenVPN Server or Client.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
<id>layer4.FromOpenvpnStaticKey</id>
|
||||
<label>OpenVPN Static Key</label>
|
||||
<type>select_multiple</type>
|
||||
<style>selectpicker style_matchers matchers_openvpn</style>
|
||||
<style>selectpicker style_openvpn</style>
|
||||
<hint>Any</hint>
|
||||
<size>5</size>
|
||||
<help><![CDATA[Select a Static Key to match. Multiple Static Keys are only supported for tls-crypt2_client mode.]]></help>
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
<id>layer4.TerminateTls</id>
|
||||
<label>Terminate TLS</label>
|
||||
<type>checkbox</type>
|
||||
<style>style_matchers matchers_domain</style>
|
||||
<style>style_domain</style>
|
||||
<help><![CDATA[Terminate TLS before routing to the upstream. Since this requires a certificate, ensure there is a domain configured in "Reverse Proxy" that matches the SNI of "Domain". The best matching SAN or wildcard certificate will be used automatically for this route.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
|
|
@ -127,35 +127,45 @@
|
|||
<style>tokenize</style>
|
||||
<allownew>true</allownew>
|
||||
<help><![CDATA[Enter a domain name or IP address of the upstream destination. If multiple are chosen, they will be load balanced with the default random policy.]]></help>
|
||||
<grid_view>
|
||||
<formatter>to_domain</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>layer4.ToPort</id>
|
||||
<label>Upstream Port</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Choose a custom port for the upstream destination.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>layer4.ProxyProtocol</id>
|
||||
<label>Proxy Protocol</label>
|
||||
<type>dropdown</type>
|
||||
<help><![CDATA[Add the HA Proxy Protocol header. Either version 1 or 2 can be chosen. The default is off, since it is only needed when the upstream can use the Proxy Protocol header.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>layer4.description</id>
|
||||
<label>Description</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Enter a description for this Layer4 route.]]></help>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Load Balancing</label>
|
||||
<advanced>true</advanced>
|
||||
<collapse>true</collapse>
|
||||
</field>
|
||||
<field>
|
||||
<id>layer4.lb_policy</id>
|
||||
<label>Load Balance Policy</label>
|
||||
<type>dropdown</type>
|
||||
<style>selectpicker style_reverse_proxy</style>
|
||||
<style>selectpicker</style>
|
||||
<help><![CDATA[lb_policy is the name of the load balancing policy, along with any options. For policies that involve hashing, the highest-random-weight (HRW) algorithm is used to ensure that a client or request with the same hash key is mapped to the same upstream, even if the list of upstreams change.]]></help>
|
||||
<advanced>true</advanced>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
|
|
@ -164,7 +174,6 @@
|
|||
<id>layer4.PassiveHealthFailDuration</id>
|
||||
<label>Passive Health Fail Duration (s)</label>
|
||||
<type>text</type>
|
||||
<style>style_reverse_proxy</style>
|
||||
<hint>off</hint>
|
||||
<help><![CDATA[fail_duration enables a passive health check when multiple destinations in "Upstream Domain" are set. It is a value that defines how long to remember a failed request. A duration of 1 or more seconds enables passive health checking. A reasonable starting point might be 30s to balance error rates with responsiveness when bringing an unhealthy upstream back online.]]></help>
|
||||
<advanced>true</advanced>
|
||||
|
|
@ -176,7 +185,6 @@
|
|||
<id>layer4.PassiveHealthMaxFails</id>
|
||||
<label>Passive Health Max Fails</label>
|
||||
<type>text</type>
|
||||
<style>style_reverse_proxy</style>
|
||||
<hint>1</hint>
|
||||
<help><![CDATA[max_fails is the maximum number of failed requests within fail_duration that are needed before considering an upstream to be down.]]></help>
|
||||
<advanced>true</advanced>
|
||||
|
|
|
|||
|
|
@ -5,17 +5,11 @@
|
|||
<type>checkbox</type>
|
||||
<help><![CDATA[Enable this domain.]]></help>
|
||||
<grid_view>
|
||||
<width>6em</width>
|
||||
<width>2em</width>
|
||||
<type>boolean</type>
|
||||
<formatter>rowtoggle</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>reverse.description</id>
|
||||
<label>Description</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Enter a description for this domain.]]></help>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Frontend</label>
|
||||
|
|
@ -25,24 +19,33 @@
|
|||
<label>Protocol</label>
|
||||
<type>dropdown</type>
|
||||
<help><![CDATA[When choosing HTTP, automatic certificate management will be disabled and all traffic to and from this domain will be unencrypted.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>reverse.FromDomain</id>
|
||||
<label>Domain</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Enter a domain name. For a base domain, use "example.com" or "opn.example.com". For a wildcard domain, use "*.example.com". Using a wildcard domain with subdomains requires a "DNS Provider" and the "DNS-01 Challenge" or a custom certificate.]]></help>
|
||||
<grid_view>
|
||||
<formatter>from_domain</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>reverse.FromPort</id>
|
||||
<label>Port</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Leave empty to use ports 80 and 443 with automatic redirection from HTTP to HTTPS or choose a custom port. Don't forget to allow these ports with a Firewall rule. If the default ports have been changed in "General Settings", leaving this empty will use the chosen alternative ports instead.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>reverse.CustomCertificate</id>
|
||||
<label>Certificate</label>
|
||||
<type>dropdown</type>
|
||||
<style>selectpicker style_tls</style>
|
||||
<style>selectpicker style_tls_reverse</style>
|
||||
<help><![CDATA[Choose ACME to get automatic certificates with the built in ACME client; no additional plugin required. The "HTTP-01", "TLS-ALPN-01" or "DNS-01" challenge will be used to get automatic "Let's Encrypt" or "ZeroSSL" certificates. Alternatively, choose a custom certificate from "System - Trust - Certificates" for this domain. Make sure the full chain has been imported.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
|
|
@ -62,7 +65,7 @@
|
|||
<id>reverse.DnsChallenge</id>
|
||||
<label>DNS-01 Challenge</label>
|
||||
<type>checkbox</type>
|
||||
<style>style_tls</style>
|
||||
<style>style_tls_reverse</style>
|
||||
<help><![CDATA[Enable the DNS-01 Challenge for this domain. Requires a "DNS Provider" in "General Settings". This is only needed for wildcard domains, or when the default challenges can not be used due to restrictive firewall policies.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
|
|
@ -81,6 +84,12 @@
|
|||
<formatter>boolean</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>reverse.description</id>
|
||||
<label>Description</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Enter a description for this domain.]]></help>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Access</label>
|
||||
|
|
|
|||
|
|
@ -5,17 +5,11 @@
|
|||
<type>checkbox</type>
|
||||
<help><![CDATA[Enable this subdomain.]]></help>
|
||||
<grid_view>
|
||||
<width>6em</width>
|
||||
<width>2em</width>
|
||||
<type>boolean</type>
|
||||
<formatter>rowtoggle</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>subdomain.description</id>
|
||||
<label>Description</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Enter a description for this subdomain.]]></help>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Frontend</label>
|
||||
|
|
@ -25,6 +19,10 @@
|
|||
<label>Domain</label>
|
||||
<type>dropdown</type>
|
||||
<help><![CDATA[Select a wildcard domain for this subdomain.]]></help>
|
||||
<grid_view>
|
||||
<visible>false</visible>
|
||||
<formatter>model_relation_domain</formatter>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>subdomain.FromDomain</id>
|
||||
|
|
@ -54,6 +52,12 @@
|
|||
<visible>false</visible>
|
||||
</grid_view>
|
||||
</field>
|
||||
<field>
|
||||
<id>subdomain.description</id>
|
||||
<label>Description</label>
|
||||
<type>text</type>
|
||||
<help><![CDATA[Enter a description for this subdomain.]]></help>
|
||||
</field>
|
||||
<field>
|
||||
<type>header</type>
|
||||
<label>Access</label>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{#
|
||||
# Copyright (c) 2023-2024 Cedrik Pischem
|
||||
# Copyright (c) 2023-2025 Cedrik Pischem
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
|
|
@ -72,7 +72,6 @@
|
|||
if (status !== "success" || data.status !== 'ok') {
|
||||
showAlert("{{ lang._('Error applying configuration: ') }}" + JSON.stringify(data), "error");
|
||||
} else {
|
||||
showAlert("{{ lang._('Configuration applied successfully.') }}", "success");
|
||||
updateServiceControlUI('caddy');
|
||||
}
|
||||
setSpinner(generalId, 'stop');
|
||||
|
|
|
|||
|
|
@ -1,181 +0,0 @@
|
|||
{#
|
||||
# Copyright (c) 2024-2025 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.
|
||||
#}
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Bootgrid Setup
|
||||
$("#{{formGridLayer4['table_id']}}").UIBootgrid({
|
||||
search:'/api/caddy/ReverseProxy/searchLayer4/',
|
||||
get:'/api/caddy/ReverseProxy/getLayer4/',
|
||||
set:'/api/caddy/ReverseProxy/setLayer4/',
|
||||
add:'/api/caddy/ReverseProxy/addLayer4/',
|
||||
del:'/api/caddy/ReverseProxy/delLayer4/',
|
||||
toggle:'/api/caddy/ReverseProxy/toggleLayer4/',
|
||||
});
|
||||
|
||||
$("#{{formGridLayer4Openvpn['table_id']}}").UIBootgrid({
|
||||
search:'/api/caddy/ReverseProxy/searchLayer4Openvpn/',
|
||||
get:'/api/caddy/ReverseProxy/getLayer4Openvpn/',
|
||||
set:'/api/caddy/ReverseProxy/setLayer4Openvpn/',
|
||||
add:'/api/caddy/ReverseProxy/addLayer4Openvpn/',
|
||||
del:'/api/caddy/ReverseProxy/delLayer4Openvpn/',
|
||||
toggle:'/api/caddy/ReverseProxy/toggleLayer4Openvpn/',
|
||||
});
|
||||
|
||||
/**
|
||||
* Displays an alert message to the user.
|
||||
*
|
||||
* @param {string} message - The message to display.
|
||||
* @param {string} [type="error"] - The type of alert (error or success).
|
||||
*/
|
||||
function showAlert(message, type = "error") {
|
||||
const alertClass = type === "error" ? "alert-danger" : "alert-success";
|
||||
const messageArea = $("#messageArea");
|
||||
|
||||
messageArea.stop(true, true).hide();
|
||||
messageArea.removeClass("alert-success alert-danger").addClass(alertClass).html(message);
|
||||
messageArea.fadeIn(500).delay(15000).fadeOut(500, function() {
|
||||
$(this).html('');
|
||||
});
|
||||
}
|
||||
|
||||
$('input, select, textarea').on('change', function() {
|
||||
$("#messageArea").hide();
|
||||
});
|
||||
|
||||
$("#reconfigureAct").SimpleActionButton({
|
||||
onPreAction: function() {
|
||||
const dfObj = new $.Deferred();
|
||||
|
||||
// Perform configuration validation
|
||||
ajaxGet("/api/caddy/service/validate", null, function(data, status) {
|
||||
if (status === "success" && data && data['status'].toLowerCase() === 'ok') {
|
||||
dfObj.resolve();
|
||||
} else {
|
||||
showAlert(data['message'], "error");
|
||||
dfObj.reject();
|
||||
}
|
||||
}).fail(function(xhr, status, error) {
|
||||
showAlert("{{ lang._('Validation request failed: ') }}" + error, "error");
|
||||
dfObj.reject();
|
||||
});
|
||||
|
||||
return dfObj.promise();
|
||||
},
|
||||
onAction: function(data, status) {
|
||||
if (status === "success" && data && data['status'].toLowerCase() === 'ok') {
|
||||
showAlert("{{ lang._('Configuration applied successfully.') }}", "{{ lang._('Apply Success') }}");
|
||||
updateServiceControlUI('caddy');
|
||||
} else {
|
||||
showAlert("{{ lang._('Action was not successful or an error occurred.') }}", "error");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$("#layer4\\.Matchers").change(function() {
|
||||
$(".style_matchers").closest('tr').hide();
|
||||
const selectedVal = $(this).val();
|
||||
|
||||
if (selectedVal === "tlssni" || selectedVal === "httphost" || selectedVal === "quicsni") {
|
||||
$(".matchers_domain").closest('tr').show();
|
||||
} else if (selectedVal === "openvpn") {
|
||||
$(".matchers_openvpn").closest('tr').show();
|
||||
}
|
||||
});
|
||||
|
||||
$("#layer4\\.Type").change(function() {
|
||||
if ($(this).val() === "global") {
|
||||
$(".style_type").closest('tr').show();
|
||||
} else {
|
||||
$(".style_type").closest('tr').hide();
|
||||
}
|
||||
});
|
||||
|
||||
updateServiceControlUI('caddy');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-header {
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
|
||||
<li id="tab-layer4" class="active"><a data-toggle="tab" href="#layer4Tab">{{ lang._('Layer4 Routes') }}</a></li>
|
||||
<li id="tab-matcher"><a data-toggle="tab" href="#matcherTab">{{ lang._('Layer7 Matcher Settings') }}</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content content-box">
|
||||
<!-- Layer4 Tab -->
|
||||
<div id="layer4Tab" class="tab-pane fade active in">
|
||||
<div style="padding-left: 16px;">
|
||||
<h1 class="custom-header">{{ lang._('Layer4 Routes') }}</h1>
|
||||
<div style="display: block;">
|
||||
{{ partial('layout_partials/base_bootgrid_table', formGridLayer4)}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer7 Tab -->
|
||||
<div id="matcherTab" class="tab-pane fade">
|
||||
<div style="padding-left: 16px;">
|
||||
<!-- OpenVPN Matcher -->
|
||||
<h1 class="custom-header">{{ lang._('OpenVPN Static Keys') }}</h1>
|
||||
<div style="display: block;">
|
||||
{{ partial('layout_partials/base_bootgrid_table', formGridLayer4Openvpn)}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Reconfigure Button -->
|
||||
<section class="page-content-main">
|
||||
<div class="content-box">
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
<button class="btn btn-primary" id="reconfigureAct"
|
||||
data-endpoint="/api/caddy/service/reconfigure"
|
||||
data-label="{{ lang._('Apply') }}"
|
||||
data-error-title="{{ lang._('Error reconfiguring Caddy') }}"
|
||||
type="button"
|
||||
></button>
|
||||
<br/><br/>
|
||||
<!-- Message Area for error/success messages -->
|
||||
<div id="messageArea" class="alert alert-info" style="display: none;"></div>
|
||||
<!-- Message Area to hint user to apply changes when data is changed in bootgrids -->
|
||||
<div id="ConfChangeMessage" class="alert alert-info" style="display: none;">
|
||||
{{ lang._('Please do not forget to apply the configuration.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4,'id':formGridLayer4['edit_dialog_id'],'label':lang._('Edit Layer4 Route')])}}
|
||||
{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4Openvpn,'id':formGridLayer4Openvpn['edit_dialog_id'],'label':lang._('Edit OpenVPN Static Key')])}}
|
||||
|
|
@ -26,89 +26,228 @@
|
|||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Bootgrid Setup
|
||||
$("#{{formGridReverseProxy['table_id']}}").UIBootgrid({
|
||||
search:'/api/caddy/ReverseProxy/searchReverseProxy/',
|
||||
get:'/api/caddy/ReverseProxy/getReverseProxy/',
|
||||
set:'/api/caddy/ReverseProxy/setReverseProxy/',
|
||||
add:'/api/caddy/ReverseProxy/addReverseProxy/',
|
||||
del:'/api/caddy/ReverseProxy/delReverseProxy/',
|
||||
toggle:'/api/caddy/ReverseProxy/toggleReverseProxy/',
|
||||
options: {
|
||||
requestHandler: addDomainFilterToRequest,
|
||||
rowCount: [4,7,14,20,50,100,-1]
|
||||
}
|
||||
});
|
||||
|
||||
$("#{{formGridSubdomain['table_id']}}").UIBootgrid({
|
||||
search:'/api/caddy/ReverseProxy/searchSubdomain/',
|
||||
get:'/api/caddy/ReverseProxy/getSubdomain/',
|
||||
set:'/api/caddy/ReverseProxy/setSubdomain/',
|
||||
add:'/api/caddy/ReverseProxy/addSubdomain/',
|
||||
del:'/api/caddy/ReverseProxy/delSubdomain/',
|
||||
toggle:'/api/caddy/ReverseProxy/toggleSubdomain/',
|
||||
options: {
|
||||
requestHandler: addDomainFilterToRequest,
|
||||
rowCount: [4,7,14,20,50,100,-1]
|
||||
}
|
||||
});
|
||||
|
||||
$("#{{formGridHandle['table_id']}}").UIBootgrid({
|
||||
search:'/api/caddy/ReverseProxy/searchHandle/',
|
||||
get:'/api/caddy/ReverseProxy/getHandle/',
|
||||
set:'/api/caddy/ReverseProxy/setHandle/',
|
||||
add:'/api/caddy/ReverseProxy/addHandle/',
|
||||
del:'/api/caddy/ReverseProxy/delHandle/',
|
||||
toggle:'/api/caddy/ReverseProxy/toggleHandle/',
|
||||
options: {
|
||||
requestHandler: addDomainFilterToRequest
|
||||
}
|
||||
});
|
||||
|
||||
$("#{{formGridAccessList['table_id']}}").UIBootgrid({
|
||||
search:'/api/caddy/ReverseProxy/searchAccessList/',
|
||||
get:'/api/caddy/ReverseProxy/getAccessList/',
|
||||
set:'/api/caddy/ReverseProxy/setAccessList/',
|
||||
add:'/api/caddy/ReverseProxy/addAccessList/',
|
||||
del:'/api/caddy/ReverseProxy/delAccessList/',
|
||||
options:{
|
||||
rowCount: [4,7,14,20,50,100,-1]
|
||||
}
|
||||
});
|
||||
|
||||
$("#{{formGridBasicAuth['table_id']}}").UIBootgrid({
|
||||
search:'/api/caddy/ReverseProxy/searchBasicAuth/',
|
||||
get:'/api/caddy/ReverseProxy/getBasicAuth/',
|
||||
set:'/api/caddy/ReverseProxy/setBasicAuth/',
|
||||
add:'/api/caddy/ReverseProxy/addBasicAuth/',
|
||||
del:'/api/caddy/ReverseProxy/delBasicAuth/',
|
||||
options:{
|
||||
rowCount: [4,7,14,20,50,100,-1]
|
||||
}
|
||||
});
|
||||
|
||||
$("#{{formGridHeader['table_id']}}").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/',
|
||||
});
|
||||
|
||||
/**
|
||||
* Modifies the search request to include domain filter.
|
||||
*
|
||||
* @param {Object} request - The original request object.
|
||||
* @returns {Object} The modified request object with domain filter.
|
||||
*/
|
||||
function addDomainFilterToRequest(request) {
|
||||
let selectedDomains = $('#reverseFilter').val();
|
||||
if (selectedDomains && selectedDomains.length > 0) {
|
||||
request['reverseUuids'] = selectedDomains.join(',');
|
||||
}
|
||||
return request;
|
||||
// Update the URL hash when tabs are clicked
|
||||
if (location.hash) {
|
||||
$(`#maintabs a[href="${location.hash}"]`).tab('show');
|
||||
}
|
||||
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
const hash = e.target.hash;
|
||||
if (history.replaceState) {
|
||||
history.replaceState(null, null, hash);
|
||||
} else {
|
||||
location.hash = hash;
|
||||
}
|
||||
});
|
||||
|
||||
$(window).on('hashchange', function () {
|
||||
$(`#maintabs a[href="${location.hash}"]`).tab('show');
|
||||
});
|
||||
|
||||
// Bootgrid Setup
|
||||
const all_grids = {};
|
||||
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
let grid_ids = [];
|
||||
|
||||
{% if entrypoint == 'reverse_proxy' %}
|
||||
|
||||
switch (e.target.hash) {
|
||||
case '#domains':
|
||||
grid_ids = ["{{ formGridReverseProxy['table_id'] }}", "{{ formGridSubdomain['table_id'] }}"];
|
||||
break;
|
||||
case '#handlers':
|
||||
grid_ids = ["{{ formGridHandle['table_id'] }}"];
|
||||
break;
|
||||
case '#access':
|
||||
grid_ids = ["{{ formGridAccessList['table_id'] }}", "{{ formGridBasicAuth['table_id'] }}"];
|
||||
break;
|
||||
case '#headers':
|
||||
grid_ids = ["{{ formGridHeader['table_id'] }}"];
|
||||
break;
|
||||
}
|
||||
|
||||
{% elseif entrypoint == 'layer4' %}
|
||||
|
||||
switch (e.target.hash) {
|
||||
case '#routes':
|
||||
grid_ids = ["{{ formGridLayer4['table_id'] }}"];
|
||||
break;
|
||||
case '#matchers':
|
||||
grid_ids = ["{{ formGridLayer4Openvpn['table_id'] }}"];
|
||||
break;
|
||||
}
|
||||
|
||||
{% endif %}
|
||||
|
||||
const labels = {
|
||||
upstream: "{{ lang._('Upstream') }}",
|
||||
domain: '<i class="fa fa-fw fa-globe text-success"></i>' + "{{ lang._('Domain') }}",
|
||||
subdomain: '<i class="fa fa-fw fa-globe text-warning"></i>' + "{{ lang._('Subdomain') }}",
|
||||
};
|
||||
|
||||
if (grid_ids.length > 0) {
|
||||
grid_ids.forEach(function(grid_id) {
|
||||
if (!all_grids[grid_id]) {
|
||||
// Define commands only for the specific grids
|
||||
let commands = {};
|
||||
|
||||
{% if entrypoint == 'reverse_proxy' %}
|
||||
|
||||
if (["{{ formGridReverseProxy['table_id'] }}", "{{ formGridSubdomain['table_id'] }}"].includes(grid_id)) {
|
||||
const update_filter = function (selectValues) {
|
||||
$('#reverseFilter')
|
||||
// Refresh selectpicker with latest data so button always works even on new domains
|
||||
.fetch_options('/api/caddy/ReverseProxy/getAllReverseDomains')
|
||||
.done(function () {
|
||||
$('#reverseFilter')
|
||||
.selectpicker('val', selectValues)
|
||||
.selectpicker('refresh')
|
||||
.trigger('change');
|
||||
});
|
||||
|
||||
$('#maintabs a[href="#handlers"]').tab('show');
|
||||
};
|
||||
|
||||
commands.search_handler = {
|
||||
method: function () {
|
||||
const rowUuid = $(this).data("row-id");
|
||||
if (!rowUuid) return;
|
||||
|
||||
update_filter([rowUuid]);
|
||||
},
|
||||
classname: 'fa fa-fw fa-search',
|
||||
title: "{{ lang._('Search Handler') }}",
|
||||
sequence: 20
|
||||
};
|
||||
|
||||
commands.add_handler = {
|
||||
method: function () {
|
||||
const rowUuid = $(this).data("row-id");
|
||||
if (!rowUuid) return;
|
||||
|
||||
open_add_dialog = function (selectValues) {
|
||||
update_filter(selectValues);
|
||||
|
||||
// Ensure selectpicker has values selected before click on add button
|
||||
$('#reverseFilter').one('changed.bs.select', function (e) {
|
||||
$("#" + "{{ formGridHandle['table_id'] }}")
|
||||
.closest('.bootgrid-box')
|
||||
.find("button[data-action='add']")
|
||||
.trigger('click');
|
||||
});
|
||||
};
|
||||
|
||||
// Resolve reverse domains, as subdomains need wildcard domain and subdomain in dialog
|
||||
if (grid_id === "{{ formGridSubdomain['table_id'] }}") {
|
||||
ajaxGet(`/api/caddy/ReverseProxy/get{{ formGridSubdomain['table_id'] }}/` + rowUuid, {}, function (rowData) {
|
||||
const reverseUuids = rowData?.subdomain?.reverse || {};
|
||||
const selectedReverse = Object.entries(reverseUuids).find(([uuid, entry]) => entry.selected === 1);
|
||||
const selectValues = selectedReverse ? [selectedReverse[0], rowUuid] : [rowUuid];
|
||||
open_add_dialog(selectValues);
|
||||
});
|
||||
} else {
|
||||
open_add_dialog([rowUuid]);
|
||||
}
|
||||
},
|
||||
classname: 'fa fa-fw fa-plus',
|
||||
title: "{{ lang._('Add Handler') }}",
|
||||
sequence: 10
|
||||
};
|
||||
}
|
||||
|
||||
{% endif %}
|
||||
|
||||
all_grids[grid_id] = $("#" + grid_id)
|
||||
.UIBootgrid({
|
||||
search: `/api/caddy/ReverseProxy/search${grid_id}/`,
|
||||
get: `/api/caddy/ReverseProxy/get${grid_id}/`,
|
||||
set: `/api/caddy/ReverseProxy/set${grid_id}/`,
|
||||
add: `/api/caddy/ReverseProxy/add${grid_id}/`,
|
||||
del: `/api/caddy/ReverseProxy/del${grid_id}/`,
|
||||
toggle: `/api/caddy/ReverseProxy/toggle${grid_id}/`,
|
||||
options: {
|
||||
requestHandler: function (request) {
|
||||
const selectedDomains = $('#reverseFilter').val();
|
||||
if (selectedDomains && selectedDomains.length > 0) {
|
||||
request['domainUuids'] = selectedDomains;
|
||||
}
|
||||
return request;
|
||||
},
|
||||
headerFormatters: {
|
||||
enabled: function (column) { return "" },
|
||||
ToDomain: function (column) { return labels.upstream; },
|
||||
FromDomain: function (column) {
|
||||
if (grid_id === "Subdomain") {
|
||||
return labels.subdomain;
|
||||
} else {
|
||||
return labels.domain;
|
||||
}
|
||||
},
|
||||
reverse: function (column) {
|
||||
return labels.domain;
|
||||
},
|
||||
subdomain: function (column) {
|
||||
return labels.subdomain;
|
||||
},
|
||||
},
|
||||
formatters: {
|
||||
model_relation_domain: function (column, row) {
|
||||
let result = (row[column.id] || "").trim();
|
||||
if (column.id === "reverse") {
|
||||
result = result.replace(" ", ":");
|
||||
if (!row["subdomain"] && row["HandlePath"]) {
|
||||
result += row["HandlePath"];
|
||||
}
|
||||
} else if (column.id === "subdomain") {
|
||||
if (row["subdomain"] && row["HandlePath"]) {
|
||||
result += row["HandlePath"];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
from_domain: function (column, row) {
|
||||
return (
|
||||
(row["DisableTls"] || "") +
|
||||
(row["FromDomain"] || "") +
|
||||
(row["FromPort"] ? `:${row["FromPort"]}` : "")
|
||||
);
|
||||
},
|
||||
to_domain: function (column, row) {
|
||||
return (
|
||||
(row["HttpTls"] || "") +
|
||||
(row["ToDomain"] || "") +
|
||||
(row["ToPort"] ? `:${row["ToPort"]}` : "") +
|
||||
(row["ToPath"] || "")
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
commands: commands
|
||||
});
|
||||
|
||||
$("#" + grid_id).wrap('<div class="bootgrid-box"></div>');
|
||||
|
||||
}
|
||||
|
||||
{% if entrypoint == 'reverse_proxy' %}
|
||||
|
||||
// insert buttons and selectpicker
|
||||
if (['{{formGridReverseProxy["table_id"]}}', '{{formGridHandle["table_id"]}}'].includes(grid_id)) {
|
||||
const header = $("#" + grid_id + "-header");
|
||||
const $actionBar = header.find('.actionBar');
|
||||
if ($actionBar.length) {
|
||||
$('#add_filter_container').detach().insertBefore($actionBar.find('.search'));
|
||||
$('#add_filter_container').show();
|
||||
}
|
||||
}
|
||||
|
||||
{% endif %}
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Displays an alert message to the user.
|
||||
*
|
||||
|
|
@ -126,54 +265,21 @@
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads domain filters from the server and populates the filter dropdown.
|
||||
*/
|
||||
function loadDomainFilters() {
|
||||
ajaxGet('/api/caddy/ReverseProxy/getAllReverseDomains', null, function(data, status) {
|
||||
let select = $('#reverseFilter');
|
||||
select.empty();
|
||||
if (status === "success" && data && data.rows) {
|
||||
data.rows.forEach(function(item) {
|
||||
select.append($('<option>').val(item.id).text(item.domainPort));
|
||||
});
|
||||
} else {
|
||||
select.html(`<option value="">{{ lang._('Failed to load data') }}</option>`);
|
||||
}
|
||||
select.selectpicker('refresh');
|
||||
}).fail(function() {
|
||||
$('#reverseFilter').html(`<option value="">{{ lang._('Failed to load data') }}</option>`).selectpicker('refresh');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls the visibility of the selectpicker and add buttons based on the active tab.
|
||||
*
|
||||
* @param {string} tab - The currently active tab.
|
||||
*/
|
||||
function toggleVisibility(tab) {
|
||||
if (tab === 'handlesTab' || tab === 'domainsTab') {
|
||||
$("#addDomainBtn").show();
|
||||
$("#addHandleBtn").show();
|
||||
$('.common-filter').show();
|
||||
} else {
|
||||
$("#addDomainBtn").hide();
|
||||
$("#addHandleBtn").hide();
|
||||
$('.common-filter').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function reloadGrids() {
|
||||
$("#{{formGridReverseProxy['table_id']}}").bootgrid("reload");
|
||||
$("#{{formGridSubdomain['table_id']}}").bootgrid("reload");
|
||||
$("#{{formGridHandle['table_id']}}").bootgrid("reload");
|
||||
}
|
||||
|
||||
// Hide message area when starting new actions
|
||||
$('input, select, textarea').on('change', function() {
|
||||
$("#messageArea").hide();
|
||||
});
|
||||
|
||||
// Populate domain filter selectpicker
|
||||
$('#reverseFilter').fetch_options('/api/caddy/ReverseProxy/getAllReverseDomains');
|
||||
|
||||
// Clear domain filter selectpicker
|
||||
$('#reverseFilterClear').on('click', function () {
|
||||
$('#reverseFilter').selectpicker('val', []);
|
||||
$('#reverseFilter').selectpicker('refresh');
|
||||
$('#reverseFilter').trigger('change');
|
||||
});
|
||||
|
||||
// Reconfigure button with custom validation
|
||||
$("#reconfigureAct").SimpleActionButton({
|
||||
onPreAction: function() {
|
||||
|
|
@ -195,7 +301,6 @@
|
|||
},
|
||||
onAction: function(data, status) {
|
||||
if (status === "success" && data && data['status'].toLowerCase() === 'ok') {
|
||||
showAlert("{{ lang._('Configuration applied successfully.') }}", "{{ lang._('Apply Success') }}");
|
||||
updateServiceControlUI('caddy');
|
||||
} else {
|
||||
showAlert("{{ lang._('Action was not successful or an error occurred.') }}", "error");
|
||||
|
|
@ -203,150 +308,186 @@
|
|||
}
|
||||
});
|
||||
|
||||
$("#clearDomains").on("click", function(e) {
|
||||
e.preventDefault();
|
||||
$('#reverseFilter').val([]);
|
||||
$('#reverseFilter').selectpicker('refresh');
|
||||
{% if entrypoint == 'reverse_proxy' %}
|
||||
|
||||
reloadGrids();
|
||||
// Safe reload on filter change, ensures all grids are initalized beforehand
|
||||
$('#reverseFilter').change(function () {
|
||||
Object.keys(all_grids).forEach(function (grid_id) {
|
||||
if ([
|
||||
'{{ formGridReverseProxy["table_id"] }}',
|
||||
'{{ formGridSubdomain["table_id"] }}',
|
||||
'{{ formGridHandle["table_id"] }}'
|
||||
].includes(grid_id)) {
|
||||
all_grids[grid_id].bootgrid('reload');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
$('#reverseFilterIcon').toggleClass('text-success', ($(this).val() || []).length > 0);
|
||||
});
|
||||
|
||||
// Reload Bootgrid on filter change
|
||||
$('#reverseFilter').on('changed.bs.select', function() {
|
||||
reloadGrids();
|
||||
// Autofill domain and subdomain when add dialog is opened
|
||||
$('#{{ formGridHandle["edit_dialog_id"] }}, #{{ formGridSubdomain["edit_dialog_id"] }}').on('opnsense_bootgrid_mapped', function(e, actionType) {
|
||||
if (actionType === 'add') {
|
||||
const selectedDomains = $('#reverseFilter').val();
|
||||
|
||||
if (selectedDomains && selectedDomains.length > 0) {
|
||||
$('#handle\\.reverse, #handle\\.subdomain, #subdomain\\.reverse')
|
||||
.selectpicker('val', selectedDomains)
|
||||
.selectpicker('refresh');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize visibility based on the active tab on page load
|
||||
let activeTab = $('#maintabs .active a').attr('href').replace('#', '');
|
||||
toggleVisibility(activeTab);
|
||||
{% endif %}
|
||||
|
||||
// Change event when switching tabs
|
||||
$('#maintabs a').on('click', function (e) {
|
||||
let currentTab = $(this).attr('href').replace('#', '');
|
||||
toggleVisibility(currentTab);
|
||||
});
|
||||
$("#handle\\.HttpTls, #handle\\.HandleDirective, #reverse\\.DisableTls, #layer4\\.Matchers, #layer4\\.Type").on("keyup change", function () {
|
||||
const http_tls = String($("#handle\\.HttpTls").val() || "")
|
||||
const handle_directive = String($("#handle\\.HandleDirective").val() || "")
|
||||
const disable_tls = String($("#reverse\\.DisableTls").val() || "")
|
||||
const layer4_matchers = String($("#layer4\\.Matchers").val() || "")
|
||||
const layer4_type = String($("#layer4\\.Type").val() || "")
|
||||
|
||||
// Add click event listener for "Add Handler" button
|
||||
$("#addHandleBtn").on("click", function() {
|
||||
if ($('#maintabs .active a').attr('href') === "#handlesTab") {
|
||||
$(`#{{formGridHandle['table_id']}} button[data-action="add"]`).click();
|
||||
} else {
|
||||
$('#maintabs a[href="#handlesTab"]').tab('show').one('shown.bs.tab', function() {
|
||||
$(`#{{formGridHandle['table_id']}} button[data-action="add"]`).click();
|
||||
const styleVisibility = [
|
||||
{
|
||||
class: "style_tls_reverse",
|
||||
visible: disable_tls === "0"
|
||||
},
|
||||
{
|
||||
class: "style_tls_handle",
|
||||
visible: http_tls === "1" && handle_directive === "reverse_proxy"
|
||||
},
|
||||
{
|
||||
class: "style_reverse_proxy",
|
||||
visible: handle_directive === "reverse_proxy"
|
||||
},
|
||||
{
|
||||
class: "style_domain",
|
||||
visible: layer4_matchers === "tlssni" || layer4_matchers === "httphost"
|
||||
},
|
||||
{
|
||||
class: "style_openvpn",
|
||||
visible: layer4_matchers === "openvpn"
|
||||
},
|
||||
{
|
||||
class: "style_type",
|
||||
visible: layer4_type === "global"
|
||||
},
|
||||
];
|
||||
|
||||
styleVisibility.forEach(style => {
|
||||
// hide/show rows with the class
|
||||
const elements = $("." + style.class).closest("tr");
|
||||
style.visible ? elements.show() : elements.hide();
|
||||
|
||||
// hide/show thead only if its parent container has the same class
|
||||
$(".table-responsive." + style.class).find("thead").each(function () {
|
||||
style.visible ? $(this).show() : $(this).hide();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add click event listener for "Add Domain" button
|
||||
$("#addDomainBtn").on("click", function() {
|
||||
if ($('#maintabs .active a').attr('href') === "#domainsTab") {
|
||||
$(`#{{formGridReverseProxy['table_id']}} button[data-action="add"]`).click();
|
||||
} else {
|
||||
$('#maintabs a[href="#domainsTab"]').tab('show').one('shown.bs.tab', function() {
|
||||
$(`#{{formGridReverseProxy['table_id']}} button[data-action="add"]`).click();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Hide TLS specific options when http or h2c is selected
|
||||
$("#handle\\.HttpTls").change(function() {
|
||||
if ($(this).val() != "1") {
|
||||
$(".style_tls").closest('tr').hide();
|
||||
} else {
|
||||
$(".style_tls").closest('tr').show();
|
||||
}
|
||||
});
|
||||
|
||||
$("#handle\\.HandleDirective").change(function() {
|
||||
if ($(this).val() === "redir") {
|
||||
$(".style_reverse_proxy").prop('disabled', true);
|
||||
$("#handle\\.header").selectpicker('refresh');
|
||||
} else {
|
||||
$(".style_reverse_proxy").prop('disabled', false);
|
||||
$("#handle\\.header").selectpicker('refresh');
|
||||
}
|
||||
});
|
||||
|
||||
// Hide TLS specific options when http is selected
|
||||
$("#reverse\\.DisableTls").change(function() {
|
||||
if ($(this).val() === "1") {
|
||||
$(".style_tls").closest('tr').hide();
|
||||
} else {
|
||||
$(".style_tls").closest('tr').show();
|
||||
}
|
||||
});
|
||||
|
||||
$("#layer4\\.Matchers").change(function() {
|
||||
if ($(this).val() !== "tlssni" && $(this).val() !== "httphost") {
|
||||
$(".style_matchers").closest('tr').hide();
|
||||
} else {
|
||||
$(".style_matchers").closest('tr').show();
|
||||
}
|
||||
});
|
||||
// Trigger bootgrid setup for handlers tab too (even if not active) to ensure command buttons always work
|
||||
$('a[href="#handlers"]').trigger('shown.bs.tab');
|
||||
|
||||
updateServiceControlUI('caddy');
|
||||
loadDomainFilters();
|
||||
$('<div id="messageArea" class="alert alert-info" style="display: none;"></div>').insertBefore('#change_message_base_form');
|
||||
$('a[data-toggle="tab"].active, #maintabs li.active a').trigger('shown.bs.tab');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.common-filter {
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
margin-right: 5px;
|
||||
padding: 0 15px;
|
||||
#add_filter_container {
|
||||
margin-left: 10px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
#add_domain_container {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#clearDomains {
|
||||
margin-top: 5px;
|
||||
#add_handle_container {
|
||||
margin-left: 10px;
|
||||
float: left;
|
||||
}
|
||||
.actionBar {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Prevent bootgrid to break out of content box*/
|
||||
.content-box {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.bootgrid-header,
|
||||
.bootgrid-box,
|
||||
.bootgrid-footer {
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
max-width: 100%;
|
||||
/* Prevents the grid from collapsing all dynamic columns completely */
|
||||
min-width: 700px;
|
||||
}
|
||||
/* Not all dropdowns support data-container="body", ensure minimal vertical space for them */
|
||||
.bootgrid-box {
|
||||
min-height: 150px;
|
||||
}
|
||||
/* Limit size of grid dropdown */
|
||||
.actions .dropdown-menu.pull-right {
|
||||
max-height: 200px;
|
||||
min-width: max-content;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
#reverseFilterClear {
|
||||
border-right: none;
|
||||
}
|
||||
#add_filter_container .bootstrap-select > .dropdown-toggle {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="add_filter_container" class="btn-group" style="display: none;">
|
||||
<button type="button" id="reverseFilterClear" class="btn btn-default" title="Clear Selection">
|
||||
<i id="reverseFilterIcon" class="fa fa-fw fa-filter-circle-xmark"></i>
|
||||
</button>
|
||||
<select id="reverseFilter" class="selectpicker form-control" multiple data-live-search="true" data-width="200px" data-size="10" data-container="body" title="{{ lang._('Filter by Domain') }}">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
|
||||
<li id="tab-domains" class="active"><a data-toggle="tab" href="#domainsTab">{{ lang._('Domains') }}</a></li>
|
||||
<li id="tab-handlers"><a data-toggle="tab" href="#handlesTab">{{ lang._('Handlers') }}</a></li>
|
||||
<li id="tab-access"><a data-toggle="tab" href="#accessTab">{{ lang._('Access') }}</a></li>
|
||||
<li id="tab-headers"><a data-toggle="tab" href="#headerTab">{{ lang._('Headers') }}</a></li>
|
||||
|
||||
{% if entrypoint == 'reverse_proxy' %}
|
||||
|
||||
<li id="tab-domains" class="active"><a data-toggle="tab" href="#domains">{{ lang._('Domains') }}</a></li>
|
||||
<li id="tab-handlers"><a data-toggle="tab" href="#handlers">{{ lang._('Handlers') }}</a></li>
|
||||
<li id="tab-access"><a data-toggle="tab" href="#access">{{ lang._('Access') }}</a></li>
|
||||
<li id="tab-headers"><a data-toggle="tab" href="#headers">{{ lang._('Headers') }}</a></li>
|
||||
|
||||
{% elseif entrypoint == 'layer4' %}
|
||||
|
||||
<li id="tab-layer4" class="active"><a data-toggle="tab" href="#routes">{{ lang._('Layer4 Routes') }}</a></li>
|
||||
<li id="tab-matcher"><a data-toggle="tab" href="#matchers">{{ lang._('Layer7 Matcher Settings') }}</a></li>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="tab-content content-box">
|
||||
<!-- Container using flexbox -->
|
||||
<div class="form-group common-filter" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<!-- Button group on the left -->
|
||||
<div>
|
||||
<button id="addDomainBtn" type="button" class="btn btn-secondary">{{ lang._('Step 1: Add Domain') }}</button>
|
||||
<button id="addHandleBtn" type="button" class="btn btn-secondary">{{ lang._('Step 2: Add Handler') }}</button>
|
||||
</div>
|
||||
<!-- Selectpicker and Clear All on the right -->
|
||||
<div class="filter-actions" style="display: flex; flex-direction: column; align-items: flex-end;">
|
||||
<select id="reverseFilter" class="selectpicker form-control" multiple data-live-search="true" data-width="334px" data-size="7" title="{{ lang._('Filter by Domain') }}">
|
||||
</select>
|
||||
<a href="#" class="text-danger" id="clearDomains" style="margin-top: 5px;">
|
||||
<i class="fa fa-times-circle"></i> <small>Clear All</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if entrypoint == 'reverse_proxy' %}
|
||||
|
||||
<!-- Combined Domains Tab -->
|
||||
<div id="domainsTab" class="tab-pane fade in active">
|
||||
<div id="domains" class="tab-pane fade in active">
|
||||
<div style="padding-left: 16px;">
|
||||
<!-- Reverse Proxy -->
|
||||
<h1 class="custom-header">{{ lang._('Domains') }}</h1>
|
||||
<div style="display: block;">
|
||||
{{ partial('layout_partials/base_bootgrid_table', formGridReverseProxy)}}
|
||||
{{ partial('layout_partials/base_bootgrid_table', formGridReverseProxy + {'command_width': '11em'})}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -354,13 +495,13 @@
|
|||
<div style="padding-left: 16px;">
|
||||
<h1 class="custom-header">{{ lang._('Subdomains') }}</h1>
|
||||
<div style="display: block;">
|
||||
{{ partial('layout_partials/base_bootgrid_table', formGridSubdomain)}}
|
||||
{{ partial('layout_partials/base_bootgrid_table', formGridSubdomain + {'command_width': '11em'})}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Handle Tab -->
|
||||
<div id="handlesTab" class="tab-pane fade">
|
||||
<div id="handlers" class="tab-pane fade">
|
||||
<div style="padding-left: 16px;">
|
||||
<h1 class="custom-header">{{ lang._('Handlers') }}</h1>
|
||||
<div style="display: block;">
|
||||
|
|
@ -370,7 +511,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Combined Access Tab -->
|
||||
<div id="accessTab" class="tab-pane fade">
|
||||
<div id="access" class="tab-pane fade">
|
||||
<!-- Access Lists Section -->
|
||||
<div style="padding-left: 16px;">
|
||||
<h1 class="custom-header">{{ lang._('Access Lists') }}</h1>
|
||||
|
|
@ -389,7 +530,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Header Tab -->
|
||||
<div id="headerTab" class="tab-pane fade">
|
||||
<div id="headers" class="tab-pane fade">
|
||||
<div style="padding-left: 16px;">
|
||||
<h1 class="custom-header">{{ lang._('Headers') }}</h1>
|
||||
<div style="display: block;">
|
||||
|
|
@ -397,29 +538,37 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reconfigure Button -->
|
||||
<section class="page-content-main">
|
||||
<div class="content-box">
|
||||
<div class="col-md-12">
|
||||
<br/>
|
||||
<button class="btn btn-primary" id="reconfigureAct"
|
||||
data-endpoint="/api/caddy/service/reconfigure"
|
||||
data-label="{{ lang._('Apply') }}"
|
||||
data-error-title="{{ lang._('Error reconfiguring Caddy') }}"
|
||||
type="button"
|
||||
></button>
|
||||
<br/><br/>
|
||||
<!-- Message Area for error/success messages -->
|
||||
<div id="messageArea" class="alert alert-info" style="display: none;"></div>
|
||||
<!-- Message Area to hint user to apply changes when data is changed in bootgrids -->
|
||||
<div id="ConfChangeMessage" class="alert alert-info" style="display: none;">
|
||||
{{ lang._('Please do not forget to apply the configuration.') }}
|
||||
{% elseif entrypoint == 'layer4' %}
|
||||
|
||||
<!-- Layer4 Tab -->
|
||||
<div id="routes" class="tab-pane fade active in">
|
||||
<div style="padding-left: 16px;">
|
||||
<h1 class="custom-header">{{ lang._('Layer4 Routes') }}</h1>
|
||||
<div style="display: block;">
|
||||
{{ partial('layout_partials/base_bootgrid_table', formGridLayer4)}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layer7 Tab -->
|
||||
<div id="matchers" class="tab-pane fade">
|
||||
<div style="padding-left: 16px;">
|
||||
<!-- OpenVPN Matcher -->
|
||||
<h1 class="custom-header">{{ lang._('OpenVPN Static Keys') }}</h1>
|
||||
<div style="display: block;">
|
||||
{{ partial('layout_partials/base_bootgrid_table', formGridLayer4Openvpn)}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/caddy/service/reconfigure'}) }}
|
||||
|
||||
{% if entrypoint == 'reverse_proxy' %}
|
||||
|
||||
{{ partial("layout_partials/base_dialog",['fields':formDialogReverseProxy,'id':formGridReverseProxy['edit_dialog_id'],'label':lang._('Edit Domain')])}}
|
||||
{{ partial("layout_partials/base_dialog",['fields':formDialogSubdomain,'id':formGridSubdomain['edit_dialog_id'],'label':lang._('Edit Subdomain')])}}
|
||||
|
|
@ -427,3 +576,10 @@
|
|||
{{ partial("layout_partials/base_dialog",['fields':formDialogAccessList,'id':formGridAccessList['edit_dialog_id'],'label':lang._('Edit Access List')])}}
|
||||
{{ partial("layout_partials/base_dialog",['fields':formDialogBasicAuth,'id':formGridBasicAuth['edit_dialog_id'],'label':lang._('Edit Basic Auth')])}}
|
||||
{{ partial("layout_partials/base_dialog",['fields':formDialogHeader,'id':formGridHeader['edit_dialog_id'],'label':lang._('Edit Header')])}}
|
||||
|
||||
{% elseif entrypoint == 'layer4' %}
|
||||
|
||||
{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4,'id':formGridLayer4['edit_dialog_id'],'label':lang._('Edit Layer4 Route')])}}
|
||||
{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4Openvpn,'id':formGridLayer4Openvpn['edit_dialog_id'],'label':lang._('Edit OpenVPN Static Key')])}}
|
||||
|
||||
{% endif %}
|
||||
|
|
|
|||
Loading…
Reference in a new issue