www/caddy: Remove CaddyCertificate.js widget in favor of using the default core provided widget (#4637)

* www/caddy: Remove CaddyCertificates.js widget in favor of using the default core provided widget.
This commit is contained in:
Monviech 2025-04-06 08:18:16 +02:00 committed by GitHub
parent 998b7b2052
commit 50d71f192a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 6 additions and 242 deletions

View file

@ -17,6 +17,7 @@ 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)
1.8.4

View file

@ -0,0 +1,4 @@
[location]
base=/var/db/caddy/data/caddy/certificates
pattern=.*\.crt$
description=Caddy

View file

@ -82,22 +82,4 @@ class DiagnosticsController extends ApiMutableModelControllerBase
return ["status" => "success", "content" => $responseArray['content']];
}
/**
* Fetch the hostnames, validity and expiration dates of automatic certificates as JSON. Consumed by Caddy widget.
*/
public function certificateAction()
{
$backend = new Backend();
$response = $backend->configdRun('caddy certificate');
// Decode JSON to PHP array
$responseArray = json_decode($response, true);
if (isset($responseArray['error'])) {
return ["status" => "failed", "message" => $responseArray['message']];
}
// Return the response as an array which gets automatically encoded to JSON
return ["status" => "success", "content" => $responseArray];
}
}

View file

@ -30,8 +30,6 @@ import sys
import json
import os
import subprocess
import asyncio
from datetime import datetime
# Function to show the Caddy configuration from a JSON file
@ -69,88 +67,11 @@ def show_caddyfile():
print(json.dumps({"error": "General Error", "message": str(e)}))
# Function to extract certificate information using openssl command
async def extract_certificate_info(cert_path):
try:
# Execute the openssl command to get the expiration date with a timeout
result = await asyncio.wait_for(
asyncio.create_subprocess_exec(
'openssl', 'x509', '-in', cert_path, '-noout', '-enddate',
stdout=subprocess.PIPE, stderr=subprocess.PIPE),
timeout=10) # Make sure tasks are cleaned up if they hang
stdout, stderr = await result.communicate()
# Check for errors in the execution
if result.returncode != 0:
error_message = stderr.decode().strip()
raise RuntimeError(f"Subprocess failed with error: {error_message}")
# Decode output and process the information
expiration_date_str = stdout.decode().strip().split('=')[1]
# Convert expiration date string to datetime object
expiration_date = datetime.strptime(expiration_date_str, "%b %d %H:%M:%S %Y GMT")
# Determine the current date
now = datetime.now()
# Calculate remaining days until expiration
remaining_days = (expiration_date - now).days
remaining_days = max(remaining_days, 0) # Ensure non-negative days
# Extract the hostname from the filename
hostname = os.path.basename(cert_path).replace('.crt', '').lower()
if hostname.startswith("wildcard_"):
hostname = hostname.replace("wildcard_", "*", 1)
return {'hostname': hostname, 'expiration_date': expiration_date_str, 'remaining_days': remaining_days}
except asyncio.TimeoutError as e:
# Handle timeout specific errors
raise RuntimeError(f"Timeout occurred while processing {cert_path}: {str(e)}")
except Exception as e:
raise RuntimeError(f"Error extracting certificate info for {cert_path}: {str(e)}")
# Function to find certificates and create tasks to extract info
async def find_certificates(base_dir):
tasks = []
for root, dirs, files in os.walk(base_dir):
# Skip any directories named 'temp'
dirs[:] = [d for d in dirs if d != 'temp']
for file in files:
if file.endswith('.crt'):
cert_path = os.path.join(root, file)
task = asyncio.create_task(extract_certificate_info(cert_path))
tasks.append(task)
if not tasks:
raise RuntimeError("No certificates were found in the specified directory.")
results = await asyncio.gather(*tasks, return_exceptions=True)
return [result for result in results if not isinstance(result, Exception)]
# Function to show certificates, processing all found in the given directory
async def show_certificates():
# Function to show certificates, processing all found in the given directory
base_dir = '/var/db/caddy/data/caddy/certificates'
try:
certificates_data = await find_certificates(base_dir)
if certificates_data:
print(json.dumps(certificates_data))
else:
raise RuntimeError("No valid certificate data found.")
except Exception as e:
print(json.dumps({"error": "General Error", "message": str(e)}))
# Action handler
def perform_action(cmd_action):
actions = {
"config": show_caddy_config,
"caddyfile": show_caddyfile,
"certificate": lambda: asyncio.run(show_certificates())
"caddyfile": show_caddyfile
}
action_func = actions.get(cmd_action)

View file

@ -47,9 +47,3 @@ command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py caddyfil
parameters:
type:script_output
message:Request Caddyfile
[certificate]
command:/usr/local/opnsense/scripts/OPNsense/Caddy/caddy_diagnostics.py certificate
parameters:
type:script_output
message:Check validity of automatic certificates

View file

@ -1,121 +0,0 @@
/*
* Copyright (C) 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.
*/
export default class CaddyCertificate extends BaseTableWidget {
constructor() {
super();
this.tickTimeout = 30;
}
getGridOptions() {
return {
// trigger overflow-y:scroll after 650px height
sizeToContent: 650
};
}
getMarkup() {
const $container = $('<div></div>');
const $caddyCertificateTable = this.createTable('caddyCertificateTable', {
headerPosition: 'none'
});
$container.append($caddyCertificateTable);
return $container;
}
async onWidgetTick() {
const proxyData = await this.ajaxCall(`/api/caddy/reverse_proxy/${'get'}`);
if (!proxyData.caddy.general || proxyData.caddy.general.enabled === "0") {
this.displayError(`${this.translations.unconfigured}`);
return;
}
const domains = Object.values(proxyData.caddy.reverseproxy?.reverse || [])
.map(proxy => proxy.FromDomain)
.filter(Boolean);
const certificates = (await this.ajaxCall(`/api/caddy/diagnostics/${'certificate'}`)).content || [];
// Display certificate if hostname in config and CN of stored cert on disk match
const matchingCertificates = certificates.filter(cert => domains.includes(cert.hostname));
if (matchingCertificates.length === 0) {
this.displayError(`${this.translations.nocerts}`);
return;
}
this.clearError();
this.processCertificates(matchingCertificates);
}
displayError(message) {
const $error = $(`<div class="error-message"><a href="/ui/caddy/general">${message}</a></div>`);
$('#caddyCertificateTable').empty().append($error);
}
clearError() {
$('#caddyCertificateTable .error-message').remove();
}
processCertificates(certificates) {
$('.caddy-certificate-tooltip').tooltip('hide');
const rows = certificates.map(certificate => {
const colorClass = certificate.remaining_days === 0
? 'text-danger'
: certificate.remaining_days < 14
? 'text-warning'
: 'text-success';
const statusText = certificate.remaining_days === 0
? this.translations.expired
: this.translations.valid;
const row = `
<div>
<i class="fa fa-lock ${colorClass} caddy-certificate-tooltip" style="cursor: pointer;"
data-tooltip="caddy-certificate-${certificate.hostname}" title="${statusText}">
</i>
&nbsp;
<span><b>${certificate.hostname}</b></span>
<br/>
<div style="margin-top: 5px; margin-bottom: 5px;">
<i>${this.translations.expires}</i> ${certificate.remaining_days} ${this.translations.days},
${new Date(certificate.expiration_date).toLocaleString()}
</div>
</div>`;
return { html: row, expirationDate: new Date(certificate.expiration_date) };
});
rows.sort((a, b) => a.expirationDate - b.expirationDate);
const sortedRows = rows.map(row => [row.html]);
super.updateTable('caddyCertificateTable', sortedRows);
$('.caddy-certificate-tooltip').tooltip({ container: 'body' });
}
}

View file

@ -13,21 +13,4 @@
<nodomains>Caddy does not manage any domains.</nodomains>
</translations>
</caddydomain>
<caddycertificate>
<filename>CaddyCertificate.js</filename>
<link>/ui/caddy/reverse_proxy</link>
<endpoints>
<endpoint>/api/caddy/diagnostics/*</endpoint>
<endpoint>/api/caddy/reverse_proxy/*</endpoint>
</endpoints>
<translations>
<title>Caddy Certificates</title>
<valid>Valid</valid>
<expired>Expired</expired>
<unconfigured>Caddy is disabled or not configured.</unconfigured>
<nocerts>Caddy does not manage any automatic certificates.</nocerts>
<expires>Expires:</expires>
<days>days</days>
</translations>
</caddycertificate>
</metadata>