summaryrefslogtreecommitdiffstats
path: root/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js
blob: 9d9af5fb1d1b0681d62093c1eb596a53f9d0fd49 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
'use strict';
'require form';
'require fs';
'require ui';
'require view';

let allSshKeys = {};
let hasSshKeygen = false;

return view.extend({
	handleSaveApply: null,
	handleSave: null,
	handleReset: null,

	load() {
		return L.resolveDefault(fs.list('/root/.ssh/'), []).then(function (entries) {
			const tasks = [
				// detect if OpenSSH ssh-keygen is installed
				L.resolveDefault(fs.stat('/usr/bin/ssh-keygen'), {}),
			];
			const sshKeyNames = _findAllPossibleIdKeys(entries);

			// read pub keys
			for (let sshKeyName of sshKeyNames) {
				const sshPubKeyName = sshKeyName + '.pub';
				tasks.push(Promise.resolve(sshKeyName));
				tasks.push(_loadPublicKey(sshPubKeyName));
			}
			return Promise.all(tasks);
		});
	},

	render(data) {
		hasSshKeygen = data[0].type === 'file';
		const sshKeys = _splitSshKeys(data.splice(1));

		let m, s;

		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 _findAllPossibleIdKeys(entries) {
	const sshKeyNames = new Set();
	const fileNames = entries.filter(item => item.type === 'file').map(item => item.name);
	for (let fileName of fileNames) {
		// a key file should have a corresponding .pub file
		if (fileName.endsWith('.pub')) {
			const sshKeyName = fileName.slice(0, -4);
			// if such a key exists then add it
			if (fileNames.includes(sshKeyName)) {
				sshKeyNames.add(sshKeyName);
			}
		} else {
			// or at least it should start with id_ e.g. id_dropbear
			if (fileName.startsWith('id_')) {
				const sshKeyName = fileName;
				sshKeyNames.add(sshKeyName);
			}
		}
	}
	return Array.from(sshKeyNames);
}

function _splitSshKeys(sshFiles) {
	const sshKeys = {};
	for (let i = 0; i < sshFiles.length; i++) {
		const sshKeyName = sshFiles[i];
		i++; // next is a .pub content
		const sshPub = sshFiles[i];
		sshKeys[sshKeyName] = '<small><code>' + sshPub + '</code></small>';
	}
	allSshKeys = sshKeys;
	return sshKeys;
}

function _renderSshKeys(sshKeys) {
	const 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')),
		])
	]);

	const rows = Object.entries(sshKeys);
	cbi_update_table(table, rows, null);

	const keyGenBtn = E('div', {}, [
		E('h4', _('Generate a new key')),
		E('form', {
			'submit': _handleKeyGenSubmit,
		}, [
			E('label', {}, _('Name') + ': '),
			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"')
		),
		table, keyGenBtn,
	]);
}

function _handleKeyGenSubmit(event) {
	event.preventDefault();
	let keyName = document.querySelector('input[name="keyName"]').value;
	keyName = keyName.startsWith('id_') ? keyName : 'id_' + keyName;
	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;
}

function _extractPubKeyFromOutput(res) {
	const lines = res.stdout.split('\n');
	for (let line of lines) {
		if (line.startsWith('ssh-')) {
			return line;
		}
	}
	return '';
}

function _loadPublicKey(sshPubKeyName) {
	return fs.read('/root/.ssh/' + sshPubKeyName)
		.catch(() => {
			// If there is no the .pub file then this is probably a Dropbear key e.g. id_dropbear.
			// We can extract it's public key by executing: dropbearkey -y -f /root/.ssh/id_dropbear
			const sshKeyName = sshPubKeyName.substring(0, sshPubKeyName.length - 4);
			return fs.exec('/usr/bin/dropbearkey', ['-y', '-f', '/root/.ssh/' + sshKeyName]).then((res) => {
				if (res.code === 0) {
					return _extractPubKeyFromOutput(res);
				} else {
					console.log('dropbearkey: ' + res.stdout + ' ' + res.stderr);
					return '';
				}
			}).catch(()=> '');
		});
}