diff options
author | Sergey Ponomarev <stokito@gmail.com> | 2023-12-04 20:27:53 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-04 19:27:53 +0100 |
commit | 7d14746ae88a83163a7e34daae70b264285bbe56 (patch) | |
tree | 177683e67b8558a259f63ed19d5fb0043b14a1fc /applications/luci-app-sshtunnel/htdocs | |
parent | 9d746c75f4023bf3c4bcfe77eaa394fbf0188d95 (diff) |
New app: luci-app-sshtunnel for SSH tunnels (#6424)
* luci-app-sshtunnel: SSH tunnels
The app helps to configure SSH tunnels.
You can also generate an SSH key and see known hosts.
Signed-off-by: Sergey Ponomarev <stokito@gmail.com>
Diffstat (limited to 'applications/luci-app-sshtunnel/htdocs')
4 files changed, 506 insertions, 0 deletions
diff --git a/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js new file mode 100644 index 0000000000..b82eae2617 --- /dev/null +++ b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js @@ -0,0 +1,64 @@ +'use strict'; +'require form'; +'require fs'; +'require ui'; +'require view'; + + +return view.extend({ + load: function () { + return Promise.all([ + fs.lines('/root/.ssh/known_hosts'), + ]); + }, + + render: function (data) { + var knownHosts = data[0]; + + var m, s, o; + + m = new form.Map('sshtunnel', _('SSH Tunnels'), + _('This configures <a %s>SSH Tunnels</a>') + .format('href="https://openwrt.org/docs/guide-user/services/ssh/sshtunnel"') + ); + + s = m.section(form.GridSection, '_known_hosts'); + s.render = L.bind(_renderKnownHosts, this, knownHosts); + + return m.render(); + }, +}); + +function _renderKnownHosts(knownHosts) { + var table = E('table', {'class': 'table cbi-section-table', 'id': 'known_hosts'}, [ + E('tr', {'class': 'tr table-titles'}, [ + E('th', {'class': 'th'}, _('Hostname')), + E('th', {'class': 'th'}, _('Public Key')), + ]) + ]); + + var rows = _splitKnownHosts(knownHosts); + cbi_update_table(table, rows); + + return E('div', {'class': 'cbi-section cbi-tblsection'}, [ + E('h3', _('Known hosts ')), + E('div', {'class': 'cbi-section-descr'}, + _('Keys of SSH servers found in %s.').format('<code>/root/.ssh/known_hosts</code>') + ), + table + ]); +} + +function _splitKnownHosts(knownHosts) { + var knownHostsMap = []; + for (var i = 0; i < knownHosts.length; i++) { + var sp = knownHosts[i].indexOf(' '); + if (sp < 0) { + continue; + } + var hostname = knownHosts[i].substring(0, sp); + var pub = knownHosts[i].substring(sp + 1); + knownHostsMap.push([hostname, '<small><code>' + pub + '</code></small>']); + } + return knownHostsMap; +} diff --git a/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js new file mode 100644 index 0000000000..6c83454b0d --- /dev/null +++ b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js @@ -0,0 +1,131 @@ +'use strict'; +'require form'; +'require fs'; +'require ui'; +'require view'; + +var allSshKeys = {}; +var hasSshKeygen = false; + +return view.extend({ + load: function () { + return L.resolveDefault(fs.list('/root/.ssh/'), []).then(function (entries) { + var tasks = [ + L.resolveDefault(fs.stat('/usr/bin/ssh-keygen'), {}), + ]; + // read pub keys + for (var i = 0; i < entries.length; i++) { + if (entries[i].type === 'file' && entries[i].name.match(/\.pub$/)) { + tasks.push(Promise.resolve(entries[i].name)); + tasks.push(fs.lines('/root/.ssh/' + entries[i].name)); + } + } + return Promise.all(tasks); + }); + }, + + render: function (data) { + hasSshKeygen = data[0].type === 'file'; + var sshKeys = _splitSshKeys(data.splice(1)); + + var m, s, o; + + m = new form.Map('sshtunnel', _('SSH Tunnels'), + _('This configures <a %s>SSH Tunnels</a>') + .format('href="https://openwrt.org/docs/guide-user/services/ssh/sshtunnel"') + ); + + s = m.section(form.GridSection, '_keys'); + s.render = L.bind(_renderSshKeys, this, sshKeys); + + return m.render(); + }, +}); + +function _splitSshKeys(sshFiles) { + var sshKeys = {}; + for (var i = 0; i < sshFiles.length; i++) { + var sshPubKeyName = sshFiles[i]; + var sshKeyName = sshPubKeyName.substring(0, sshPubKeyName.length - 4); + i++; + var sshPub = sshFiles[i]; + sshKeys[sshKeyName] = '<small><code>' + sshPub + '</code></small>'; + } + allSshKeys = sshKeys; + return sshKeys; +} + +function _renderSshKeys(sshKeys) { + var table = E('table', {'class': 'table cbi-section-table', 'id': 'keys_table'}, [ + E('tr', {'class': 'tr table-titles'}, [ + E('th', {'class': 'th'}, _('Name')), + E('th', {'class': 'th'}, _('Public Key')), + ]) + ]); + + var rows = Object.entries(sshKeys); + cbi_update_table(table, rows, null); + + var keyGenBtn = E('div', {}, [ + E('form', { + 'submit': _handleKeyGenSubmit, + }, [ + E('label', {}, _('Generate a new key') + ': '), + E('span', {'class': 'control-group'}, [ + E('input', { + 'type': 'text', + 'name': 'keyName', + 'value': 'id_ed25519', + 'pattern': '^[a-zA-Z][a-zA-Z0-9_\.]+', + 'required': 'required', + 'maxsize': '35', + 'autocomplete': 'off', + }), + E('button', { + 'id': 'btnGenerateKey', + 'type': 'submit', + 'class': 'btn cbi-button cbi-button-action', + }, [_('Generate')]) + ]) + ]) + ]); + return E('div', {'class': 'cbi-section cbi-tblsection'}, [ + E('h3', _('SSH Keys')), + E('div', {'class': 'cbi-section-descr'}, + _('Add the pub key to %s or %s.') + .format('<code>/root/.ssh/authorized_keys</code>', '<code>/etc/dropbear/authorized_keys</code>') + ' ' + + _('In LuCI you can do that with <a %s>System / Administration / SSH-Keys</a>') + .format('href="/cgi-bin/luci/admin/system/admin/sshkeys"') + ), + keyGenBtn, table + ]); +} + +function _handleKeyGenSubmit(event) { + event.preventDefault(); + var keyName = document.querySelector('input[name="keyName"]').value; + if (allSshKeys[keyName]) { + document.body.scrollTop = document.documentElement.scrollTop = 0; + ui.addNotification(null, E('p', _('A key with that name already exists.'), 'error')); + return false; + } + + let command = '/usr/bin/ssh-keygen'; + let commandArgs = ['-t', 'ed25519', '-q', '-N', '', '-f', '/root/.ssh/' + keyName]; + if (!hasSshKeygen) { + command = '/usr/bin/dropbearkey'; + commandArgs = ['-t', 'ed25519', '-f', '/root/.ssh/' + keyName]; + } + fs.exec(command, commandArgs).then(function (res) { + if (res.code === 0) { + // refresh the page to see the new key + location.reload(); + } else { + throw new Error(res.stdout + ' ' + res.stderr); + } + }).catch(function (e) { + document.body.scrollTop = document.documentElement.scrollTop = 0; + ui.addNotification(null, E('p', _('Unable to generate a key: %s').format(e.message)), 'error'); + }); + return false; +} diff --git a/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js new file mode 100644 index 0000000000..5aa33e6df8 --- /dev/null +++ b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js @@ -0,0 +1,147 @@ +'use strict'; +'require form'; +'require fs'; +'require uci'; +'require ui'; +'require view'; + +var allSshKeys = {}; + +return view.extend({ + load: function () { + return L.resolveDefault(fs.list('/root/.ssh/'), []).then(function (entries) { + var tasks = []; + for (var i = 0; i < entries.length; i++) { + if (entries[i].type === 'file' && entries[i].name.match(/\.pub$/)) { + tasks.push(Promise.resolve(entries[i].name)); + } + } + return Promise.all(tasks); + }); + }, + + render: function (data) { + var sshKeys = _splitSshKeys(data); + if (sshKeys.length === 0) { + ui.addNotification(null, E('p', _('No SSH keys found, <a %s>generate a new one</a>').format('href="./ssh_keys"')), 'warning'); + } + + var m, s, o; + + m = new form.Map('sshtunnel', _('SSH Tunnels'), + _('This configures <a %s>SSH Tunnels</a>') + .format('href="https://openwrt.org/docs/guide-user/services/ssh/sshtunnel"') + ); + + s = m.section(form.GridSection, 'server', _('Servers')); + s.anonymous = false; + s.addremove = true; + s.nodescriptions = true; + + o = s.tab('general', _('General Settings')); + o = s.tab('advanced', _('Advanced Settings')); + + o = s.taboption('general', form.Value, 'hostname', _('Hostname')); + o.placeholder = 'example.com'; + o.datatype = 'host'; + o.rmempty = false; + + o = s.taboption('general', form.Value, 'port', _('Port')); + o.placeholder = '22'; + o.datatype = 'port'; + + o = s.taboption('general', form.Value, 'user', _('User')); + o.default = 'root'; + + o = s.taboption('general', form.ListValue, 'IdentityFile', _('Key file'), + _('Private key file with authentication identity. ' + + 'See <em>ssh_config IdentityFile</em>') + ); + o.value(''); + Object.keys(sshKeys).forEach(function (keyName) { + o.value('/root/.ssh/' + keyName, keyName); + }); + o.optional = true; + + + o = s.taboption('advanced', form.ListValue, 'LogLevel', _('Log level'), 'See <em>ssh_config LogLevel</em>'); + o.value('QUIET', 'QUIET'); + o.value('FATAL', 'FATAL'); + o.value('ERROR', 'ERROR'); + o.value('INFO', 'INFO'); + o.value('VERBOSE', 'VERBOSE'); + o.value('DEBUG', 'DEBUG'); + o.value('DEBUG2', 'DEBUG2'); + o.value('DEBUG3', 'DEBUG3'); + o.default = 'INFO'; + o.modalonly = true; + + o = s.taboption('advanced', form.ListValue, 'Compression', _('Use compression'), + _('Compression may be useful on slow connections. ' + + 'See <em>ssh_config Compression</em>') + ); + o.value('yes', _('Yes')); + o.value('no', _('No')); + o.default = 'no'; + o.modalonly = true; + + o = s.taboption('advanced', form.Value, 'retrydelay', _('Retry delay'), + _('Delay after a connection failure before trying to reconnect.') + ); + o.placeholder = '10'; + o.default = '10'; + o.datatype = 'uinteger'; + o.optional = true; + o.modalonly = true; + + o = s.taboption('advanced', form.Value, 'ServerAliveCountMax', _('Server alive count max'), + _('The number of server alive messages which may be sent before SSH disconnects from the server. ' + + 'See <em>ssh_config ServerAliveCountMax</em>') + ); + o.placeholder = '3'; + o.datatype = 'uinteger'; + o.optional = true; + o.modalonly = true; + + o = s.taboption('advanced', form.Value, 'ServerAliveInterval', _('Server alive interval'), + _('Keep-alive interval (seconds). ' + + 'See <em>ssh_config ServerAliveInterval</em>') + ); + o.optional = true; + o.default = '60'; + o.datatype = 'uinteger'; + o.modalonly = true; + + o = s.taboption('advanced', form.ListValue, 'CheckHostIP', _('Check host IP'), + _('Check the host IP address in the <code>known_hosts</code> file. ' + + 'This allows ssh to detect whether a host key changed due to DNS spoofing. ' + + 'See <em>ssh_config CheckHostIP</em>') + ); + o.value('yes', _('Yes')); + o.value('no', _('No')); + o.default = 'no'; + o.modalonly = true; + + o = s.taboption('advanced', form.ListValue, 'StrictHostKeyChecking', _('Strict host key checking'), + _('Refuse to connect to hosts whose host key has changed. ' + + 'See <em>ssh_config StrictHostKeyChecking</em>')); + o.value('accept-new', _('Accept new and check if not changed')); + o.value('yes', _('Yes')); + o.value('no', _('No')); + o.default = 'accept-new'; + o.modalonly = true; + + return m.render(); + }, +}); + +function _splitSshKeys(sshFiles) { + var sshKeys = {}; + for (var i = 0; i < sshFiles.length; i++) { + var sshPubKeyName = sshFiles[i]; + var sshKeyName = sshPubKeyName.substring(0, sshPubKeyName.length - 4); + sshKeys[sshKeyName] = ''; + } + allSshKeys = sshKeys; + return sshKeys; +} diff --git a/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js new file mode 100644 index 0000000000..34fceab2f0 --- /dev/null +++ b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js @@ -0,0 +1,164 @@ +'use strict'; +'require form'; +'require fs'; +'require uci'; +'require ui'; +'require view'; + +return view.extend({ + load: function () { + return Promise.all([ + uci.load('sshtunnel'), + ]); + }, + + render: function (data) { + var m, s, o; + + m = new form.Map('sshtunnel', _('SSH Tunnels'), + _('This configures <a %s>SSH Tunnels</a>') + .format('href="https://openwrt.org/docs/guide-user/services/ssh/sshtunnel"') + ); + + s = m.section(form.GridSection, 'tunnelR', _('Remote Tunnels'), + _('Forward a port on the remote host to a service on the local host.') + ); + s.anonymous = true; + s.addremove = true; + s.nodescriptions = true; + + o = s.option(form.Flag, 'enabled', _('Enabled')); + o.default = '1'; + + o = _addServerOption(s); + + o = s.option(form.Value, 'remoteaddress', _('Remote address'), + _('Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. <code>localhost</code>.') + '<br/>' + + _('<code>*</code> means to listen all interfaces <b>including public</b>.') + ); + o.datatype = 'or(host, "*")'; + o.default = '*'; + o.rmempty = false; + + o = s.option(form.Value, 'remoteport', _('Remote port')); + o.placeholder = '80'; + o.datatype = 'port'; + o.rmempty = false; + + o = s.option(form.Value, 'localaddress', _('Local address'), + _('Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. <code>localhost</code>.') + ); + o.datatype = 'host'; + o.default = 'localhost'; + o.rmempty = false; + + o = s.option(form.Value, 'localport', _('Local port')); + o.datatype = 'port'; + o.placeholder = '80'; + o.rmempty = false; + + + s = m.section(form.GridSection, 'tunnelL', _('Local Tunnels'), + _('Forward a port on the local host to a service on the remote host.') + ); + s.anonymous = true; + s.addremove = true; + s.nodescriptions = true; + + o = s.option(form.Flag, 'enabled', _('Enabled')); + o.default = '1'; + + o = _addServerOption(s); + + o = s.option(form.Value, 'localaddress', _('Local address'), + _('Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. <code>localhost</code>.') + '<br/>' + + _('<code>*</code> means to listen all interfaces <b>including public</b>.') + ); + o.datatype = 'or(host, "*")'; + o.placeholder = '192.168.1.1'; // not the default * public iface because a user must explicitly configure it + o.rmempty = false; + + o = s.option(form.Value, 'localport', _('Local port')); + o.datatype = 'port'; + o.placeholder = '80'; + o.rmempty = false; + + o = s.option(form.Value, 'remoteaddress', _('Remote address'), + _('Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. <code>localhost</code>.') + ); + o.datatype = 'host'; + o.default = 'localhost'; + o.rmempty = false; + + o = s.option(form.Value, 'remoteport', _('Remote port')); + o.datatype = 'port'; + o.default = '80'; + o.rmempty = false; + + + s = m.section(form.GridSection, 'tunnelD', _('Dynamic Tunnels'), + _('SOCKS proxy via remote host.') + ); + s.anonymous = true; + s.addremove = true; + s.nodescriptions = true; + + o = s.option(form.Flag, 'enabled', _('Enabled')); + o.default = '1'; + + o = _addServerOption(s); + + o = s.option(form.Value, 'localaddress', _('Local address'), + _('Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. <code>localhost</code>.') + '<br/>' + + _('<code>*</code> means to listen all interfaces <b>including public</b>.') + ); + o.datatype = 'or(host, "*")'; + o.placeholder = '192.168.1.1'; // not the default * public iface because a user must explicitly configure it + o.rmempty = false; + + o = s.option(form.Value, 'localport', _('Local port')); + o.datatype = 'port'; + o.default = '1080'; + o.rmempty = false; + + + s = m.section(form.GridSection, 'tunnelW', _('VPN Tunnels'), + _('Configure TUN/TAP devices for VPN tunnels.') + ); + s.anonymous = true; + s.addremove = true; + s.nodescriptions = true; + + o = s.option(form.Flag, 'enabled', _('Enabled')); + o.default = '1'; + + o = _addServerOption(s); + + o = s.option(form.ListValue, 'vpntype', _('VPN type')); + o.value('point-to-point', 'TUN (point-to-point)'); + o.value('ethernet', 'TAP (ethernet)'); + o.default = 'point-to-point'; + + o = s.option(form.Value, 'localdev', _('Local dev')); + o.default = 'any'; + o.datatype = 'or("any", min(0))'; + o.rmempty = false; + + o = s.option(form.Value, 'remotedev', _('Remote dev')); + o.default = 'any'; + o.datatype = 'or("any", min(0))'; + o.rmempty = false; + + return m.render(); + }, +}); + +function _addServerOption(s) { + var o = s.option(form.ListValue, 'server', _('Server')); + o.datatype = 'uciname'; + o.rmempty = false; + uci.sections('sshtunnel', 'server', function (s, sectionName) { + o.value(sectionName, s.hostname ? '%s (%s)'.format(sectionName, s.hostname) : sectionName); + }); + return o; +} |