v8.1.8: Add SSL certificate management feature

Introduced support for uploading, viewing, and deleting SSL certificates via a dedicated web interface. Updated routing, templates, and backend logic to handle certificate types (pem, key, crt) and improved integration with SSL-related UI components.
pull/418/head
Aidaho 2025-04-16 18:41:21 +03:00
parent c153da9842
commit 3ca015d279
10 changed files with 298 additions and 37 deletions

View File

@ -723,7 +723,7 @@ def update_db_v_8_1_6():
def update_ver():
try:
Version.update(version='8.1.7').execute()
Version.update(version='8.1.8').execute()
except Exception:
print('Cannot update version')

View File

@ -339,9 +339,17 @@ def get_ssl_raw_cert(server_ip: str, cert_id: str) -> str:
return f'error: Cannot connect to the server {e}'
def get_ssl_certs(server_ip: str) -> str:
def get_ssl_certs(server_ip: str, cert_type: str = None) -> str:
cert_path = sql.get_setting('cert_path')
command = f"sudo ls -1t {cert_path} |grep -E 'pem|crt|key'"
if cert_type == 'pem':
cert_type = 'pem'
elif cert_type == 'crt':
cert_type = 'crt'
elif cert_type == 'key':
cert_type = 'key'
else:
cert_type = 'pem|crt|key'
command = f"sudo ls -1t {cert_path} |grep -E '{cert_type}'"
try:
return server_mod.ssh_command(server_ip, command)
except Exception as e:
@ -358,7 +366,7 @@ def del_ssl_cert(server_ip: str, cert_id: str) -> str:
return f'error: Cannot delete the certificate {e}'
def upload_ssl_cert(server_ip: str, ssl_name: str, ssl_cont: str) -> list[str]:
def upload_ssl_cert(server_ip: str, ssl_name: str, ssl_cont: str, ssl_type: str = 'pem') -> list[str]:
cert_path = sql.get_setting('cert_path')
tmp_path = sql.get_setting('tmp_config_path')
output = []
@ -367,8 +375,8 @@ def upload_ssl_cert(server_ip: str, ssl_name: str, ssl_cont: str) -> list[str]:
if ssl_name is None:
raise Exception('Please enter a desired name')
else:
name = f"{ssl_name}.pem"
path_to_file = f"{tmp_path}/{ssl_name}.pem"
name = f"{ssl_name}.{ssl_type}"
path_to_file = f"{tmp_path}/{ssl_name}.{ssl_type}"
try:
with open(path_to_file, "w") as ssl_cert:

View File

@ -304,6 +304,7 @@ class SSLCertUploadRequest(BaseModel):
server_ip: Union[IPvAnyAddress, DomainName]
name: EscapedString
cert: EscapedString
cert_type: Literal['key', 'crt', 'pem'] = 'pem'
class SavedServerRequest(BaseModel):

View File

@ -136,6 +136,13 @@ def last_edit(service: str, server_ip: Union[IPvAnyAddress, DomainName]):
return service_common.get_overview_last_edit(str(server_ip), service)
@bp.route('/<service>/ssl')
@check_services
@get_user_params()
def ssl_service(service):
return render_template('ssl.html', lang=g.user_params['lang'])
@bp.route('/cpu-ram-metrics/<server_ip>/<server_id>/<name>/<service>')
@get_user_params()
@validate()

View File

@ -1,3 +1,160 @@
$( function() {
$("#ssl_key_or_crt_upload").click(function () {
if (!checkIsServerFiled('#serv6')) return false;
if (!checkIsServerFiled('#ssl_key_name', 'Enter the Certificate name')) return false;
if (!checkIsServerFiled('#ssl_key_or_crt', 'Paste the contents of the certificate file')) return false;
let jsonData = {
server_ip: $('#serv6').val(),
cert_type: $('#new-cert-file-type').val(),
cert: $('#ssl_key_or_crt').val(),
name: $('#ssl_key_name').val()
}
$.ajax({
url: "/add/cert/add",
data: JSON.stringify(jsonData),
contentType: "application/json; charset=utf-8",
type: "POST",
success: function (data) {
if (data.error === 'failed') {
toastr.error(data.error);
} else {
for (let i = 0; i < data.length; i++) {
if (data[i]) {
if (data[i].indexOf('error: ') != '-1' || data[i].indexOf('Errno') != '-1') {
toastr.error(data[i]);
} else {
toastr.success(data[i]);
}
}
}
}
}
});
});
$('#ssl_key_view').click(function () {
if (!checkIsServerFiled('#serv5')) return false;
$.ajax({
url: "/add/certs/" + $('#serv5').val(),
success: function (data) {
if (data.indexOf('error:') != '-1') {
toastr.error(data);
} else {
let i;
let new_data = "";
data = data.split("\n");
let j = 1
for (i = 0; i < data.length; i++) {
data[i] = data[i].replace(/\s+/g, ' ');
if (data[i] != '') {
if (j % 2) {
if (j != 0) {
new_data += '</span>'
}
new_data += '<span class="list_of_lists">'
} else {
new_data += '</span><span class="list_of_lists">'
}
j += 1
new_data += ' <a onclick="view_ssl(\'' + data[i] + '\')" title="View ' + data[i] + ' cert">' + data[i] + '</a> '
}
}
$("#ajax-show-ssl").html(new_data);
}
}
});
});
});
function view_ssl(id) {
let raw_word = translate_div.attr('data-raw');
if(!checkIsServerFiled('#serv5')) return false;
$.ajax( {
url: "/add/cert/" + $('#serv5').val() + '/' + id,
success: function( data ) {
if (data.indexOf('error: ') != '-1') {
toastr.error(data);
} else {
$('#dialog-confirm-body').text(data);
$( "#dialog-confirm-cert" ).dialog({
resizable: false,
height: "auto",
width: 670,
modal: true,
title: "Certificate from "+$('#serv5').val()+", name: "+id,
buttons: [{
text: cancel_word,
click: function () {
$(this).dialog("close");
}
}, {
text: raw_word,
click: function () {
showRawSSL(id);
}
}, {
text: delete_word,
click: function () {
$(this).dialog("close");
confirmDeleting("SSL cert", id, $(this), "");
}
}]
});
}
}
} );
}
function showRawSSL(id) {
$.ajax({
url: "/add/cert/get/raw/" + $('#serv5').val() + "/" + id,
success: function (data) {
if (data.indexOf('error: ') != '-1') {
toastr.error(data);
} else {
$('#dialog-confirm-body').text(data);
$("#dialog-confirm-cert").dialog({
resizable: false,
height: "auto",
width: 670,
modal: true,
title: "Certificate from " + $('#serv5').val() + ", name: " + id,
buttons: [{
text: cancel_word,
click: function () {
$(this).dialog("close");
}
}, {
text: "Human readable",
click: function () {
view_ssl(id);
}
}, {
text: delete_word,
click: function () {
$(this).dialog("close");
confirmDeleting("SSL cert", id, $(this), "");
}
}]
});
}
}
});
}
function deleteSsl(id) {
if (!checkIsServerFiled('#serv5')) return false;
$.ajax({
url: "/add/cert/" + $("#serv5").val() + "/" + id,
type: "DELETE",
success: function (data) {
if (data.indexOf('error: ') != '-1') {
toastr.error(data);
} else {
toastr.clear();
toastr.success('SSL cert ' + id + ' has been deleted');
$("#ssl_key_view").trigger("click");
}
}
});
}
let provides = {'standalone': "Stand alone", 'route53': 'Route53', 'linode': 'Linode', 'cloudflare': 'Cloudflare', 'digitalocean': 'Digitalocean'};
$( function() {
let typeSelect = $( "#new-le-type" );
@ -220,3 +377,4 @@ function showLe(data) {
])
$('#le_table_body').append(le_tag);
}

View File

@ -98,6 +98,9 @@
showCompareConfigs();
}
if (cur_url[6] === 'show') {
if (cur_url[4] === 'nginx') {
showConfigFiles(false, cur_url[7]);
}
showConfig();
}
if (cur_url[6] === 'show-files') {
@ -116,21 +119,21 @@
}
if (cur_url[4] === 'haproxy' && cur_url[6] === 'edit') {
var myCodeMirror = CodeMirror.fromTextArea(document.getElementById("config_text_area"),
{
mode: "haproxy",
lineNumbers: true,
lineWrapping: true,
autocapitalize: true,
autocorrect: true,
spellcheck: true,
autoCloseBrackets: true,
keyMap: "sublime",
matchBrackets: true,
foldGutter: true,
showCursorWhenSelecting: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "breakpoints"],
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: true}
});
{
mode: "haproxy",
lineNumbers: true,
lineWrapping: true,
autocapitalize: true,
autocorrect: true,
spellcheck: true,
autoCloseBrackets: true,
keyMap: "sublime",
matchBrackets: true,
foldGutter: true,
showCursorWhenSelecting: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "breakpoints"],
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: true}
});
} else if (cur_url[6] === 'edit') {
var myCodeMirror = CodeMirror.fromTextArea(document.getElementById("config_text_area"),
{
@ -149,15 +152,17 @@
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: true}
});
}
myCodeMirror.on("gutterClick", function(cm, n) {
let info = cm.lineInfo(n);
cm.setGutterMarker(n, "breakpoints", info.gutterMarkers ? null : makeMarker());
});
myCodeMirror.on("beforeChange", function (cm, change) {
$(window).bind('beforeunload', function(){
return 'Are you sure you want to leave?';
});
});
if (cur_url[6] === 'edit') {
myCodeMirror.on("gutterClick", function (cm, n) {
let info = cm.lineInfo(n);
cm.setGutterMarker(n, "breakpoints", info.gutterMarkers ? null : makeMarker());
});
myCodeMirror.on("beforeChange", function (cm, change) {
$(window).bind('beforeunload', function () {
return 'Are you sure you want to leave?';
});
});
}
function makeMarker() {
var marker = document.createElement("div");
marker.style.color = "#822";
@ -172,7 +177,9 @@
}
</style>
<script>
myCodeMirror.refresh();
if (cur_url[6] === 'edit') {
myCodeMirror.refresh();
}
$(document).bind('keydown', 'ctrl+s', function (){
$("[type='submit'][value='save']").click();
});

View File

@ -197,6 +197,7 @@
<script>
{% for cluster in clusters %}
getHaCluster('{{cluster.id}}');
setInterval(getHaCluster, 600000, '{{cluster.id}}');
{% endfor %}
</script>
{% endif %}

View File

@ -40,7 +40,7 @@
<span title="{{lang.words.create|title()}} SSL {{lang.words.listener|title()}}" class="redirectListen span-link" id="create-ssl-listen">{{lang.words.create|title()}} SSL {{lang.words.listener|title()}}</span>
</div>
<div class="server-desc add_proxy">
{{lang.add_page.desc.create_ssl_proxy}} <span title="Upload SSL" class="redirectSsl span-link" style="color: #5d9ceb">{{lang.words.uploaded}} {{lang.words.w_a}} PEM {{lang.words.cert}}</span>
{{lang.add_page.desc.create_ssl_proxy}} <a href="{{ url_for('service.ssl_service', service='haproxy') }}" target="_blank">{{lang.words.uploaded}} {{lang.words.w_a}} PEM {{lang.words.cert}}</a>
</div>
</div>
</div>
@ -90,7 +90,7 @@
<span title="{{lang.words.create|title()}} SSL {{lang.words.frontend|title()}}" class="redirectListen span-link" id="create-ssl-frontend">{{lang.words.create|title()}} SSL {{lang.words.frontend|title()}}</span>
</div>
<div class="server-desc add_proxy">
{{lang.add_page.desc.create_ssl_front}} <span title="{{lang.words.upload|title()}} SSL" class="redirectSsl span-link" style="color: #5d9ceb">{{lang.words.uploaded}} {{lang.words.w_a}} PEM {{lang.words.cert}}</span>
{{lang.add_page.desc.create_ssl_front}} <a href="{{ url_for('service.ssl_service', service='haproxy') }}" target="_blank">{{lang.words.uploaded}} {{lang.words.w_a}} PEM {{lang.words.cert}}</a>
</div>
</div>
</div>
@ -140,7 +140,7 @@
<span title="{{lang.words.create|title()}} SSL {{lang.words.backend|title()}}" class="redirectListen span-link" id="create-ssl-backend">{{lang.words.create|title()}} SSL {{lang.words.backend|title()}}</span>
</div>
<div class="server-desc add_proxy">
{{lang.add_page.desc.create_ssl_backend}} <span title="{{lang.words.upload|title()}} SSL" class="redirectSsl span-link" style="color: #5d9ceb">{{lang.words.uploaded}} {{lang.words.w_a}} PEM {{lang.words.cert}}</span>
{{lang.add_page.desc.create_ssl_backend}} <a href="{{ url_for('service.ssl_service', service='haproxy') }}" target="_blank">{{lang.words.uploaded}} {{lang.words.w_a}} PEM {{lang.words.cert}}</a>
</div>
</div>
</div>

View File

@ -39,7 +39,7 @@
{% if g.user_params['role'] <= 3 %}
<li><a href="{{ url_for('add.add', service='haproxy') }}#proxy" title="{{lang.menu_links.add_proxy.title}}" class="head-submenu" id="add1">{{lang.menu_links.add_proxy.link}}</a></li>
<li><a href="{{ url_for('config.versions', service='haproxy') }}" title="{{lang.menu_links.versions.haproxy.title}}" class="head-submenu">{{lang.menu_links.versions.link}}</a></li>
<li><a href="{{ url_for('add.add', service='haproxy') }}#ssl" title="{{lang.menu_links.ssl.title}}" class="head-submenu" id="add3">{{lang.menu_links.ssl.link}}</a></li>
<li><a href="{{ url_for('service.ssl_service', service='haproxy') }}" title="{{lang.menu_links.ssl.title}}" class="head-submenu" id="add3">{{lang.menu_links.ssl.link}}</a></li>
<li><a href="{{ url_for('add.add', service='haproxy') }}#lists" title="{{lang.menu_links.lists.title}}" class="head-submenu" id="add7">{{lang.menu_links.lists.link}}</a></li>
<li><a href="{{ url_for('waf.waf', service='haproxy') }}" title="Web application firewall" class="head-submenu">WAF</a> </li>
{% endif %}
@ -61,7 +61,7 @@
{% if g.user_params['role'] <= 3 %}
<li><a href="{{ url_for('add.add', service='nginx') }}#proxy" title="{{lang.menu_links.add_proxy.title}}" class="head-submenu">{{lang.menu_links.add_proxy.link}}</a></li>
<li><a href="{{ url_for('config.versions', service='nginx') }}" title="{{lang.menu_links.versions.nginx.title}}" class="head-submenu">{{lang.menu_links.versions.link}}</a></li>
<li><a href="{{ url_for('add.add', service='haproxy') }}?service=nginx#ssl" title="{{lang.menu_links.ssl.title}}" class="head-submenu">{{lang.menu_links.ssl.link}}</a></li>
<li><a href="{{ url_for('service.ssl_service', service='nginx') }}" title="{{lang.menu_links.ssl.title}}" class="head-submenu">{{lang.menu_links.ssl.link}}</a></li>
<li><a href="{{ url_for('waf.waf', service='nginx') }}" title="Web application firewall" class="head-submenu">WAF</a> </li>
{% endif %}
</ul>
@ -78,7 +78,7 @@
<li><a href="{{ url_for('metric.metrics', service='apache') }}" title="Apache {{lang.menu_links.metrics.title}}" class="head-submenu">{{lang.menu_links.metrics.link}}</a></li>
{% if g.user_params['role'] <= 3 %}
<li><a href="{{ url_for('config.versions', service='apache') }}" title="{{lang.menu_links.versions.apache.title}}" class="head-submenu">{{lang.menu_links.versions.link}}</a></li>
<li><a href="{{ url_for('add.add', service='haproxy') }}?service=apache#ssl" title="{{lang.menu_links.ssl.title}}" class="head-submenu" id="add3">{{lang.menu_links.ssl.link}}</a></li>
<li><a href="{{ url_for('service.ssl_service', service='apache') }}" title="{{lang.menu_links.ssl.title}}" class="head-submenu" id="add3">{{lang.menu_links.ssl.link}}</a></li>
{% endif %}
</ul>
</li>

79
app/templates/ssl.html Normal file
View File

@ -0,0 +1,79 @@
{% extends "base.html" %}
{% block title %}{{lang.words.add|title()}} SSL{% endblock %}
{% block h2 %}{{lang.words.add|title()}} SSL {% endblock %}
{% block content %}
{% from 'include/input_macros.html' import input, checkbox, select %}
<script src="/static/js/add.js"></script>
<script src="/static/js/ssl.js"></script>
<table>
<caption><h3>SSL</h3></caption>
<tr class="overviewHead">
<td class="padding10 first-collumn" style="width: 30%;">{{lang.words.view|title()}} {{lang.words.cert2}}</td>
<td>
{{lang.words.uploaded|title()}} {{lang.words.certs}}
</td>
<td></td>
<td></td>
</tr>
<tr>
<td class="padding10 first-collumn">
{{ select('serv5', values=g.user_params['servers'], is_servers='true', by_id='true') }}
<button id="ssl_key_view" title="{{lang.words.view|title()}} {{lang.words.certs}}">{{lang.words.view|title()}}</button>
</td>
<td colspan="2" style="padding: 10px 0 10px 0;">
<span id="ajax-show-ssl"></span>
</td>
<td></td>
</tr>
<tr class="overviewHead">
<td class="padding10 first-collumn" style="width: 30%;">{{lang.words.upload|title()}} SSL {{lang.words.certs}}</td>
<td>
{{lang.words.cert_name|title()}}
</td>
<td>
<span title="{{lang.words.file|title()}} {{ lang.words.type }}" class="help_cursor">{{lang.words.file|title()}} {{ lang.words.type }}</span>
</td>
<td>
<span title="{{lang.add_page.paste_cert_desc}}" class="help_cursor">{{lang.add_page.desc.paste_cert}}</span>
</td>
</tr>
<tr>
<td class="first-collumn padding10" valign="top" style="padding-top: 15px;">
{{ select('serv6', values=g.user_params['servers'], is_servers='true') }}
</td>
<td valign="top" style="padding-top: 27px;">
{{ input('ssl_key_name') }}
</td>
<td valign="top" style="padding-top: 27px;">
<select id="new-cert-file-type">
<option value="pem">Pem</option>
<option value="key">Key</option>
<option value="crt">Crt (chain)</option>
</select>
</td>
<td style="padding-top: 15px; padding-bottom: 15px;">
<textarea id="ssl_key_or_crt" cols="50" rows="5"></textarea><br /><br />
<button id="ssl_key_or_crt_upload" title="{{lang.words.upload|title()}} SSL {{lang.words.cert}}">{{lang.words.upload|title()}}</button>
</td>
</tr>
</table>
<table id="le_table">
<caption><h3>Let's Encrypt</h3></caption>
<tr class="overviewHead">
<td class="padding10 first-collumn">{{lang.words.server|title()}}</td>
<td>{{lang.words.type|title()}}</td>
<td>{{lang.words.domains|title()}}</td>
<td>{{lang.words.desc|title()}}</td>
<td></td>
<td></td>
</tr>
<tbody id="le_table_body"></tbody>
</table>
<br /><span class="add-button" title="{{lang.words.create|title()}}" onclick="openLeDialog()">+ {{lang.words.create|title()}}</span>
<div id="ajax-ssl"></div>
{% include 'include/del_confirm.html' %}
<div id="dialog-confirm-cert" title="View certificate " style="display: none;">
<pre id="dialog-confirm-body"></pre>
</div>
<input type="hidden" id="group_id" value="{{ g.user_params['group_id']|string() }}">
{% endblock %}