--- /dev/null
+Ext.define('Proxmox.panel.Certificates', {
+ extend: 'Ext.grid.Panel',
+ xtype: 'pmxCertificates',
+
+ // array of { name, id (=filename), url, deletable, reloadUi }
+ uploadButtons: undefined,
+
+ // The /info path for the current node.
+ infoUrl: undefined,
+
+ columns: [
+ {
+ header: gettext('File'),
+ width: 150,
+ dataIndex: 'filename',
+ },
+ {
+ header: gettext('Issuer'),
+ flex: 1,
+ dataIndex: 'issuer',
+ },
+ {
+ header: gettext('Subject'),
+ flex: 1,
+ dataIndex: 'subject',
+ },
+ {
+ header: gettext('Public Key Alogrithm'),
+ flex: 1,
+ dataIndex: 'public-key-type',
+ hidden: true,
+ },
+ {
+ header: gettext('Public Key Size'),
+ flex: 1,
+ dataIndex: 'public-key-bits',
+ hidden: true,
+ },
+ {
+ header: gettext('Valid Since'),
+ width: 150,
+ dataIndex: 'notbefore',
+ renderer: Proxmox.Utils.render_timestamp,
+ },
+ {
+ header: gettext('Expires'),
+ width: 150,
+ dataIndex: 'notafter',
+ renderer: Proxmox.Utils.render_timestamp,
+ },
+ {
+ header: gettext('Subject Alternative Names'),
+ flex: 1,
+ dataIndex: 'san',
+ renderer: Proxmox.Utils.render_san,
+ },
+ {
+ header: gettext('Fingerprint'),
+ dataIndex: 'fingerprint',
+ hidden: true,
+ },
+ {
+ header: gettext('PEM'),
+ dataIndex: 'pem',
+ hidden: true,
+ },
+ ],
+
+ reload: function() {
+ let me = this;
+ me.rstore.load();
+ },
+
+ delete_certificate: function() {
+ let me = this;
+
+ let rec = me.selModel.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ let cert = me.certById[rec.id];
+ let url = cert.url;
+ Proxmox.Utils.API2Request({
+ url: `/api2/extjs/${url}?restart=1`,
+ method: 'DELETE',
+ success: function(response, opt) {
+ if (cert.reloadUid) {
+ let txt =
+ gettext('GUI will be restarted with new certificates, please reload!');
+ Ext.getBody().mask(txt, ['x-mask-loading']);
+ // reload after 10 seconds automatically
+ Ext.defer(function() {
+ window.location.reload(true);
+ }, 10000);
+ }
+ },
+ failure: function(response, opt) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ view_certificate: function() {
+ let me = this;
+ let view = me.getView();
+
+ let selection = view.getSelection();
+ if (!selection || selection.length < 1) {
+ return;
+ }
+ let win = Ext.create('Proxmox.window.CertificateViewer', {
+ cert: selection[0].data.filename,
+ url: `/api2/extjs/${view.infoUrl}`,
+ });
+ win.show();
+ },
+ },
+
+ listeners: {
+ itemdblclick: 'view_certificate',
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ // only used for the store name
+ me.nodename = "_all";
+ }
+
+ if (!me.uploadButtons) {
+ throw "no upload buttons defined";
+ }
+
+ if (!me.infoUrl) {
+ throw "no certificate store url given";
+ }
+
+ me.rstore = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'certs-' + me.nodename,
+ model: 'proxmox-certificate',
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/extjs/${me.infoUrl}`,
+ },
+ });
+
+ me.store = {
+ type: 'diff',
+ rstore: me.rstore,
+ };
+
+ let tbar = [];
+
+ me.deletableCertIds = {};
+ me.certById = {};
+ if (me.uploadButtons.length === 1) {
+ let cert = me.uploadButtons[0];
+
+ if (!cert.url) {
+ throw "missing certificate url";
+ }
+
+ me.certById[cert.id] = cert;
+
+ if (cert.deletable) {
+ me.deletableCertIds[cert.id] = true;
+ }
+
+ tbar.push(
+ {
+ xtype: 'button',
+ text: gettext('Upload Custom Certificate'),
+ handler: function() {
+ let grid = this.up('grid');
+ let win = Ext.create('Proxmox.window.CertificateUpload', {
+ url: `/api2/extjs/${cert.url}`,
+ reloadUi: cert.reloadUi,
+ });
+ win.show();
+ win.on('destroy', grid.reload, grid);
+ },
+ },
+ );
+ } else {
+ let items = [];
+
+ me.selModel = Ext.create('Ext.selection.RowModel', {});
+
+ for (const cert of me.uploadButtons) {
+ if (!cert.id) {
+ throw "missing id in certificate entry";
+ }
+
+ if (!cert.url) {
+ throw "missing url in certificate entry";
+ }
+
+ if (!cert.name) {
+ throw "missing name in certificate entry";
+ }
+
+ me.certById[cert.id] = cert;
+
+ if (cert.deletable) {
+ me.deletableCertIds[cert.id] = true;
+ }
+
+ items.push({
+ text: Ext.String.format('Upload {0} Certificate', cert.name),
+ handler: function() {
+ let grid = this.up('grid');
+ let win = Ext.create('Proxmox.window.CertificateUpload', {
+ url: `/api2/extjs/${cert.url}`,
+ reloadUi: cert.reloadUi,
+ });
+ win.show();
+ win.on('destroy', grid.reload, grid);
+ },
+ });
+ }
+
+ tbar.push(
+ {
+ text: gettext('Upload Custom Certificate'),
+ menu: {
+ xtype: 'menu',
+ items,
+ },
+ },
+ );
+ }
+
+ tbar.push(
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Delete Custom Certificate'),
+ confirmMsg: rec => Ext.String.format(
+ gettext('Are you sure you want to remove the certificate used for {0}'),
+ me.certById[rec.id].name,
+ ),
+ callback: () => me.reload(),
+ selModel: me.selModel,
+ disabled: true,
+ enableFn: rec => !!me.deletableCertIds[rec.id],
+ handler: function() { me.delete_certificate(); },
+ },
+ '-',
+ {
+ xtype: 'proxmoxButton',
+ itemId: 'viewbtn',
+ disabled: true,
+ text: gettext('View Certificate'),
+ handler: 'view_certificate',
+ },
+ );
+ Ext.apply(me, { tbar });
+
+ me.callParent();
+
+ me.rstore.startUpdate();
+ me.on('destroy', me.rstore.stopUpdate, me.rstore);
+ },
+});
--- /dev/null
+Ext.define('Proxmox.window.CertificateViewer', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pmxCertViewer',
+
+ title: gettext('Certificate'),
+
+ fieldDefaults: {
+ labelWidth: 120,
+ },
+ width: 800,
+ resizable: true,
+
+ items: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Name'),
+ name: 'filename',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Fingerprint'),
+ name: 'fingerprint',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Issuer'),
+ name: 'issuer',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Subject'),
+ name: 'subject',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Public Key Type'),
+ name: 'public-key-type',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Public Key Size'),
+ name: 'public-key-bits',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Valid Since'),
+ renderer: Proxmox.Utils.render_timestamp,
+ name: 'notbefore',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Expires'),
+ renderer: Proxmox.Utils.render_timestamp,
+ name: 'notafter',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Subject Alternative Names'),
+ name: 'san',
+ renderer: Proxmox.Utils.render_san,
+ },
+ {
+ xtype: 'textarea',
+ editable: false,
+ grow: true,
+ growMax: 200,
+ fieldLabel: gettext('Certificate'),
+ name: 'pem',
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.cert) {
+ throw "no cert given";
+ }
+
+ if (!me.url) {
+ throw "no url given";
+ }
+
+ me.callParent();
+
+ // hide OK/Reset button, because we just want to show data
+ me.down('toolbar[dock=bottom]').setVisible(false);
+
+ me.load({
+ success: function(response) {
+ if (Ext.isArray(response.result.data)) {
+ Ext.Array.each(response.result.data, function(item) {
+ if (item.filename === me.cert) {
+ me.setValues(item);
+ return false;
+ }
+ return true;
+ });
+ }
+ },
+ });
+ },
+});
+
+Ext.define('Proxmox.window.CertificateUpload', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pmxCertUpload',
+
+ title: gettext('Upload Custom Certificate'),
+ resizable: false,
+ isCreate: true,
+ submitText: gettext('Upload'),
+ method: 'POST',
+ width: 600,
+
+ // whether the UI needs a reload after this
+ reloadUi: undefined,
+
+ apiCallDone: function(success, response, options) {
+ let me = this;
+
+ if (!success || !me.reloadUi) {
+ return;
+ }
+
+ var txt = gettext('GUI server will be restarted with new certificates, please reload!');
+ Ext.getBody().mask(txt, ['pve-static-mask']);
+ // reload after 10 seconds automatically
+ Ext.defer(function() {
+ window.location.reload(true);
+ }, 10000);
+ },
+
+ items: [
+ {
+ fieldLabel: gettext('Private Key (Optional)'),
+ labelAlign: 'top',
+ emptyText: gettext('No change'),
+ name: 'key',
+ xtype: 'textarea',
+ },
+ {
+ xtype: 'filebutton',
+ text: gettext('From File'),
+ listeners: {
+ change: function(btn, e, value) {
+ let form = this.up('form');
+ e = e.event;
+ Ext.Array.each(e.target.files, function(file) {
+ Proxmox.Utils.loadTextFromFile(
+ file,
+ function(res) {
+ form.down('field[name=key]').setValue(res);
+ },
+ 16384,
+ );
+ });
+ btn.reset();
+ },
+ },
+ },
+ {
+ xtype: 'box',
+ autoEl: 'hr',
+ },
+ {
+ fieldLabel: gettext('Certificate Chain'),
+ labelAlign: 'top',
+ allowBlank: false,
+ name: 'certificates',
+ xtype: 'textarea',
+ },
+ {
+ xtype: 'filebutton',
+ text: gettext('From File'),
+ listeners: {
+ change: function(btn, e, value) {
+ let form = this.up('form');
+ e = e.event;
+ Ext.Array.each(e.target.files, function(file) {
+ Proxmox.Utils.loadTextFromFile(
+ file,
+ function(res) {
+ form.down('field[name=certificates]').setValue(res);
+ },
+ 16384,
+ );
+ });
+ btn.reset();
+ },
+ },
+ },
+ {
+ xtype: 'hidden',
+ name: 'restart',
+ value: '1',
+ },
+ {
+ xtype: 'hidden',
+ name: 'force',
+ value: '1',
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.url) {
+ throw "neither url given";
+ }
+
+ me.callParent();
+ },
+});