summaryrefslogtreecommitdiffhomepage
path: root/applications/luci-app-sshtunnel/htdocs
diff options
context:
space:
mode:
authorSergey Ponomarev <stokito@gmail.com>2023-12-04 20:27:53 +0200
committerGitHub <noreply@github.com>2023-12-04 19:27:53 +0100
commit7d14746ae88a83163a7e34daae70b264285bbe56 (patch)
tree177683e67b8558a259f63ed19d5fb0043b14a1fc /applications/luci-app-sshtunnel/htdocs
parent9d746c75f4023bf3c4bcfe77eaa394fbf0188d95 (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')
-rw-r--r--applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js64
-rw-r--r--applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js131
-rw-r--r--applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js147
-rw-r--r--applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js164
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;
+}