mirror of
https://github.com/opnsense/plugins.git
synced 2026-06-09 08:56:23 -04:00
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:
parent
998b7b2052
commit
50d71f192a
7 changed files with 6 additions and 242 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
4
www/caddy/src/etc/ssl/ext_sources/caddy.conf
Normal file
4
www/caddy/src/etc/ssl/ext_sources/caddy.conf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[location]
|
||||
base=/var/db/caddy/data/caddy/certificates
|
||||
pattern=.*\.crt$
|
||||
description=Caddy
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
<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' });
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue