diff --git a/src/Makefile b/src/Makefile index d0435b8..d782e92 100644 --- a/src/Makefile +++ b/src/Makefile @@ -49,6 +49,7 @@ JSSRC= \ panel/PruneKeepPanel.js \ panel/RRDChart.js \ panel/GaugeWidget.js \ + panel/Certificates.js \ window/Edit.js \ window/PasswordEdit.js \ window/SafeDestroy.js \ @@ -56,6 +57,7 @@ JSSRC= \ window/LanguageEdit.js \ window/DiskSmart.js \ window/ZFSDetail.js \ + window/Certificates.js \ node/APT.js \ node/NetworkEdit.js \ node/NetworkView.js \ diff --git a/src/panel/Certificates.js b/src/panel/Certificates.js new file mode 100644 index 0000000..332a189 --- /dev/null +++ b/src/panel/Certificates.js @@ -0,0 +1,267 @@ +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); + }, +}); diff --git a/src/window/Certificates.js b/src/window/Certificates.js new file mode 100644 index 0000000..df6dae3 --- /dev/null +++ b/src/window/Certificates.js @@ -0,0 +1,213 @@ +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(); + }, +});