tabby-rdp 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +580 -0
- package/README.md +252 -34
- package/dist/accountForm.js +0 -1
- package/dist/accounts.js +0 -1
- package/dist/audio.js +0 -1
- package/dist/authorities.js +331 -0
- package/dist/byteQueue.js +102 -0
- package/dist/clipboard.js +106 -0
- package/dist/connectionStatus.js +0 -1
- package/dist/desk.js +16 -47
- package/dist/deskScript.js +196 -6
- package/dist/desktop.service.js +1763 -204
- package/dist/desktopForm.js +72 -13
- package/dist/desktops.js +243 -19
- package/dist/drives.js +525 -0
- package/dist/fileTransfer.js +1064 -72
- package/dist/gateway.js +651 -0
- package/dist/header.js +76 -5
- package/dist/help.js +34 -13
- package/dist/hyperv.js +129 -0
- package/dist/index.js +2 -2
- package/dist/keyboard.js +0 -1
- package/dist/microphone.js +57 -13
- package/dist/ntlm.js +225 -0
- package/dist/osd.js +56 -15
- package/dist/probe.js +15 -3
- package/dist/rdcleanpath.js +468 -52
- package/dist/rdpFile.js +53 -4
- package/dist/rdpProfile.js +75 -22
- package/dist/remoteSetup.js +103 -23
- package/dist/sessionLog.js +53 -0
- package/dist/settingsPage.js +117 -27
- package/dist/signin.js +429 -54
- package/dist/ssh.js +99 -18
- package/dist/sshChannelStream.js +50 -6
- package/dist/targets.js +185 -39
- package/dist/ui.js +98 -20
- package/dist/unshowable.js +41 -0
- package/dist/updates.js +37 -2
- package/dist/vms.js +36 -5
- package/dist/wake.js +31 -12
- package/package.json +4 -3
- package/remote/trd-pty.py +74 -13
- package/vendor/SHA256SUMS +26 -0
- package/vendor/iron-remote-desktop-rdp.js +418 -390
- package/vendor/ironrdp_web_bg.wasm +0 -0
- package/dist/accountForm.js.map +0 -1
- package/dist/accounts.js.map +0 -1
- package/dist/audio.js.map +0 -1
- package/dist/connectionStatus.js.map +0 -1
- package/dist/desk.js.map +0 -1
- package/dist/deskScript.js.map +0 -1
- package/dist/desktop.service.js.map +0 -1
- package/dist/desktopForm.js.map +0 -1
- package/dist/desktops.js.map +0 -1
- package/dist/fileTransfer.js.map +0 -1
- package/dist/header.js.map +0 -1
- package/dist/help.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keyboard.js.map +0 -1
- package/dist/microphone.js.map +0 -1
- package/dist/osd.js.map +0 -1
- package/dist/probe.js.map +0 -1
- package/dist/rdcleanpath.js.map +0 -1
- package/dist/rdpFile.js.map +0 -1
- package/dist/rdpProfile.js.map +0 -1
- package/dist/remoteSetup.js.map +0 -1
- package/dist/settingsPage.js.map +0 -1
- package/dist/signin.js.map +0 -1
- package/dist/ssh.js.map +0 -1
- package/dist/sshChannelStream.js.map +0 -1
- package/dist/targets.js.map +0 -1
- package/dist/ui.js.map +0 -1
- package/dist/updates.js.map +0 -1
- package/dist/vms.js.map +0 -1
- package/dist/wake.js.map +0 -1
package/dist/rdpFile.js
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MICROPHONE_NOTE = void 0;
|
|
3
4
|
exports.parseRdpFile = parseRdpFile;
|
|
5
|
+
/**
|
|
6
|
+
* The note for a file that asks for the microphone: not applied while the setting is off. With it on, every desktop
|
|
7
|
+
* gets the microphone, and the import says so instead (see RemoteDesktopService.importRdp).
|
|
8
|
+
*/
|
|
9
|
+
exports.MICROPHONE_NOTE = 'the microphone (Settings › Remote Desktop › Microphone)';
|
|
10
|
+
/** What a setting in the file would mean here, when it isn't applied: by key, for the values that ask for something. */
|
|
11
|
+
const NOTES = {
|
|
12
|
+
'audiomode': v => v === '1' ? 'sound left on the remote (sound plays here; Settings › Remote Desktop › Sound)' : v === '2' ? 'sound off (Settings › Remote Desktop › Sound)' : null,
|
|
13
|
+
'audiocapturemode': v => v === '1' ? exports.MICROPHONE_NOTE : null,
|
|
14
|
+
'redirectdrives': v => v === '1' ? 'its drives (share folders in Settings › Remote Desktop › Shared folders)' : null,
|
|
15
|
+
'drivestoredirect': v => v ? 'its drives (share folders in Settings › Remote Desktop › Shared folders)' : null,
|
|
16
|
+
'redirectprinters': v => v === '1' ? 'printer redirection (not supported)' : null,
|
|
17
|
+
'redirectsmartcards': v => v === '1' ? 'smart card redirection (not supported)' : null,
|
|
18
|
+
'redirectcomports': v => v === '1' ? 'serial port redirection (not supported)' : null,
|
|
19
|
+
'usbdevicestoredirect': v => v ? 'USB redirection (not supported)' : null,
|
|
20
|
+
'use multimon': v => v === '1' ? 'several monitors (one screen per desktop here)' : null,
|
|
21
|
+
'screen mode id': (v, all) => v === '1' && (all.get('desktopwidth') || all.get('desktopheight'))
|
|
22
|
+
? `a fixed ${all.get('desktopwidth') ?? '?'}×${all.get('desktopheight') ?? '?'} window (the desktop follows the pane; Settings › Remote Desktop › When the pane is resized)` : null,
|
|
23
|
+
'gatewayhostname': (v, all) => v && !usesGateway(all) ? `the RD Gateway ${v}, which the file names but doesn't use (the profile's RD Gateway field takes it)` : null,
|
|
24
|
+
'promptcredentialonce': (v, all) => v === '0' && usesGateway(all) ? 'an account of its own for the gateway (pick a saved account as the profile\'s Gateway account)' : null,
|
|
25
|
+
'gatewaycredentialssource': (v, all) => (v === '1' || v === '5') && usesGateway(all) ? `a ${v === '1' ? 'smart card' : 'sign-in page'} at the gateway (not supported; it is signed in to with a user name and password)` : null,
|
|
26
|
+
'remoteapplicationmode': v => v === '1' ? 'RemoteApp (not supported; the whole desktop opens)' : null,
|
|
27
|
+
'alternate shell': v => v ? 'a program to start instead of the desktop (not supported)' : null,
|
|
28
|
+
'enablecredsspsupport': v => v === '0' ? 'sign-in without Network Level Authentication (it is always used)' : null,
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Whether the file connects through its gateway: it names one, and `gatewayusagemethod` doesn't say not to (0, or 4:
|
|
32
|
+
* never; 1 to 3: always, when a direct connection fails, by the user's defaults). mstsc always writes the method; a
|
|
33
|
+
* file written by hand that only names a gateway means it.
|
|
34
|
+
*/
|
|
35
|
+
function usesGateway(settings) {
|
|
36
|
+
return !!settings.get('gatewayhostname') && !['0', '4'].includes(settings.get('gatewayusagemethod') ?? '');
|
|
37
|
+
}
|
|
4
38
|
/** mstsc writes UTF-16LE with a byte order mark; other tools write UTF-8, with or without one. */
|
|
5
39
|
function decode(data) {
|
|
6
40
|
const b = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
@@ -22,26 +56,41 @@ function decode(data) {
|
|
|
22
56
|
/** The connection a .rdp file describes, or null without a usable `full address`. */
|
|
23
57
|
function parseRdpFile(data) {
|
|
24
58
|
const settings = new Map();
|
|
59
|
+
// A limit is kept whichever way the file writes it (`0`, `00`, `+0`), and wherever it is: a file that says it
|
|
60
|
+
// twice, differently, gets the narrower of the two rather than whichever line comes last.
|
|
61
|
+
let clipboardOff = false;
|
|
25
62
|
for (const line of decode(data).split(/\r?\n/)) {
|
|
26
63
|
const m = /^\s*([^:]+?)\s*:\s*([sib])\s*:(.*)$/i.exec(line);
|
|
27
64
|
if (m) {
|
|
28
|
-
|
|
65
|
+
const key = m[1].toLowerCase();
|
|
66
|
+
const value = m[3].trim();
|
|
67
|
+
settings.set(key, value);
|
|
68
|
+
clipboardOff ||= key === 'redirectclipboard' && /^[+-]?\d+$/.test(value) && Number(value) === 0;
|
|
29
69
|
}
|
|
30
70
|
}
|
|
31
71
|
const full = settings.get('full address') ?? '';
|
|
32
|
-
// host, host:port, [v6]:port, or a bare IPv6 address (colons, no brackets).
|
|
33
|
-
|
|
72
|
+
// host, host:port, [v6]:port, or a bare IPv6 address (colons, no brackets). No '#': session and trust keys are
|
|
73
|
+
// split on it, so a host that carried one could be taken for another desktop's key (see keyParts).
|
|
74
|
+
const m = /^\[([^\]#]+)\](?::(\d+))?$|^([^:\s#]+)(?::(\d+))?$/.exec(full);
|
|
34
75
|
const host = m?.[1] ?? m?.[3] ?? (/^[0-9a-f:.]+$/i.test(full) && (full.match(/:/g)?.length ?? 0) > 1 ? full : null);
|
|
35
76
|
// A port in the address wins over `server port`, as in mstsc.
|
|
36
77
|
const port = Number(m?.[2] ?? m?.[4] ?? (settings.get('server port') || 3389));
|
|
37
78
|
if (!host || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
38
79
|
return null;
|
|
39
80
|
}
|
|
81
|
+
const scale = Number(settings.get('desktopscalefactor'));
|
|
82
|
+
// By own property only: a file whose key is `__proto__` or `constructor` names an inherited member of the NOTES
|
|
83
|
+
// object (Object.prototype, the Object constructor), not a note, and calling one would throw or inject its text.
|
|
84
|
+
const noteFor = (key) => Object.prototype.hasOwnProperty.call(NOTES, key) ? NOTES[key] : undefined;
|
|
85
|
+
const ignored = [...settings].map(([key, value]) => noteFor(key)?.(value, settings) ?? null).filter((n) => !!n);
|
|
40
86
|
return {
|
|
41
87
|
host,
|
|
42
88
|
port,
|
|
43
89
|
...settings.get('username') ? { username: settings.get('username') } : {},
|
|
44
90
|
...settings.get('domain') ? { domain: settings.get('domain') } : {},
|
|
91
|
+
...Number.isFinite(scale) && scale >= 100 ? { sharpness: scale >= 150 ? 'retina' : 'standard' } : {},
|
|
92
|
+
...usesGateway(settings) ? { gateway: settings.get('gatewayhostname') } : {},
|
|
93
|
+
...clipboardOff ? { clipboard: 'off' } : {},
|
|
94
|
+
ignored: [...new Set(ignored)],
|
|
45
95
|
};
|
|
46
96
|
}
|
|
47
|
-
//# sourceMappingURL=rdpFile.js.map
|
package/dist/rdpProfile.js
CHANGED
|
@@ -18,8 +18,10 @@ const desktopForm_1 = require("./desktopForm");
|
|
|
18
18
|
const help_1 = require("./help");
|
|
19
19
|
const accounts_1 = require("./accounts");
|
|
20
20
|
const accountForm_1 = require("./accountForm");
|
|
21
|
+
const clipboard_1 = require("./clipboard");
|
|
21
22
|
const desktops_1 = require("./desktops");
|
|
22
|
-
const
|
|
23
|
+
const gateway_1 = require("./gateway");
|
|
24
|
+
const osd_1 = require("./osd");
|
|
23
25
|
const ssh_1 = require("./ssh");
|
|
24
26
|
const targets_1 = require("./targets");
|
|
25
27
|
/** Input of an SSH tab opened for an RDP profile with `via`: the desktop to show once SSH is connected. */
|
|
@@ -133,7 +135,7 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
133
135
|
const o = this.profile.options;
|
|
134
136
|
const account = () => `${o.username ?? ''}\n${o.domain ?? ''}\n${o.account ?? ''}`;
|
|
135
137
|
// No id for a new profile (no address yet): it has nothing saved to take along.
|
|
136
|
-
this.before = { id: o.host ? (0, desktops_1.desktopIdOf)(o) : '', account: account(), via: o.via ?? '' };
|
|
138
|
+
this.before = { id: o.host ? (0, desktops_1.desktopIdOf)(o) : '', account: account(), via: o.via ?? '', gateway: o.gateway ?? '' };
|
|
137
139
|
const root = this.element.nativeElement;
|
|
138
140
|
root.innerHTML = `
|
|
139
141
|
<div class="mb-3">
|
|
@@ -167,6 +169,23 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
167
169
|
<div class="mb-3" data-own-account>
|
|
168
170
|
<label>Domain</label>
|
|
169
171
|
<input class="form-control" name="domain" spellcheck="false" placeholder="Optional">
|
|
172
|
+
</div>
|
|
173
|
+
<div class="mb-3">
|
|
174
|
+
<label>RD Gateway</label>
|
|
175
|
+
<input class="form-control" name="gateway" spellcheck="false" placeholder="Optional: a Remote Desktop Gateway, e.g. rdgw.example.com">
|
|
176
|
+
<div class="text-danger small" data-for="gateway"></div>
|
|
177
|
+
<div class="text-muted small">The desktop is then reached through the gateway (HTTPS, port 443 unless given), and the address above is as the gateway sees it.</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="mb-3" data-gateway-account>
|
|
180
|
+
<label>Gateway account</label>
|
|
181
|
+
<select class="form-control" name="gatewayAccount"></select>
|
|
182
|
+
<div class="text-muted small">A gateway that takes another account than the desktop: a saved account for it.</div>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="mb-3">
|
|
185
|
+
<label>Clipboard</label>
|
|
186
|
+
<select class="form-control" name="clipboard"></select>
|
|
187
|
+
<div class="text-muted small">Which ways text, pictures and files copied on either side go between this computer and the desktop.
|
|
188
|
+
Applies on the next connection.</div>
|
|
170
189
|
</div>`;
|
|
171
190
|
const field = (name) => root.querySelector(`[name="${name}"]`);
|
|
172
191
|
field('address').value = o.host ? (0, desktopForm_1.formatAddress)(o.host, o.port || 3389) : '';
|
|
@@ -208,6 +227,31 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
208
227
|
ownAccount();
|
|
209
228
|
field('username').addEventListener('input', () => { o.username = field('username').value.trim(); });
|
|
210
229
|
field('domain').addEventListener('input', () => { o.domain = field('domain').value.trim(); });
|
|
230
|
+
// The gateway, and its account where that isn't the desktop's (offered once there is a gateway and a saved account).
|
|
231
|
+
const gateway = field('gateway');
|
|
232
|
+
const gatewayAccount = field('gatewayAccount');
|
|
233
|
+
gateway.value = o.gateway ?? '';
|
|
234
|
+
gatewayAccount.append(new Option('The same as the desktop\'s', ''), ...accounts.map(a => new Option(label(a), a.id)));
|
|
235
|
+
if (o.gatewayAccount && !accounts.some(a => a.id === o.gatewayAccount)) {
|
|
236
|
+
gatewayAccount.append(new Option('(a saved account that no longer exists)', o.gatewayAccount));
|
|
237
|
+
}
|
|
238
|
+
gatewayAccount.value = o.gatewayAccount ?? '';
|
|
239
|
+
const gatewayChanged = () => {
|
|
240
|
+
const text = gateway.value.trim();
|
|
241
|
+
root.querySelector('[data-for=gateway]').textContent = text && !(0, gateway_1.parseGateway)(text) ? 'A name or address, with :port if it isn\'t 443.' : '';
|
|
242
|
+
root.querySelector('[data-gateway-account]').hidden = !text || gatewayAccount.options.length < 2;
|
|
243
|
+
};
|
|
244
|
+
gateway.addEventListener('input', () => {
|
|
245
|
+
o.gateway = gateway.value.trim();
|
|
246
|
+
gatewayChanged();
|
|
247
|
+
});
|
|
248
|
+
gatewayAccount.addEventListener('change', () => { o.gatewayAccount = gatewayAccount.value; });
|
|
249
|
+
gatewayChanged();
|
|
250
|
+
// Its own clipboard sharing, or the setting's (named, as it is now).
|
|
251
|
+
const clipboard = field('clipboard');
|
|
252
|
+
clipboard.append(new Option(`As in Settings › Remote Desktop (${clipboard_1.CLIPBOARD_LABELS[desktop.settings().clipboard].toLowerCase()})`, ''), ...Object.entries(clipboard_1.CLIPBOARD_LABELS).map(([mode, text]) => new Option(text, mode)));
|
|
253
|
+
clipboard.value = (0, clipboard_1.ownClipboard)(o.clipboard) ?? '';
|
|
254
|
+
clipboard.addEventListener('change', () => { o.clipboard = clipboard.value; });
|
|
211
255
|
const via = field('via');
|
|
212
256
|
via.addEventListener('change', () => { o.via = via.value; });
|
|
213
257
|
this.injector.get(tabby_core_1.ProfilesService).getProfiles().then(profiles => {
|
|
@@ -221,21 +265,22 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
221
265
|
}
|
|
222
266
|
save() {
|
|
223
267
|
const o = this.profile.options;
|
|
224
|
-
const { id, account, via } = this.before;
|
|
268
|
+
const { id, account, via, gateway } = this.before;
|
|
225
269
|
if (!id) {
|
|
226
270
|
return;
|
|
227
271
|
}
|
|
228
272
|
if ((o.via ?? '') !== via) {
|
|
229
|
-
// Reached another way, it is another desktop. A direct one's saved
|
|
230
|
-
// behind a host shares them with that host's
|
|
273
|
+
// Reached another way, it is another desktop. A direct one's saved passwords (through any gateway, the ones
|
|
274
|
+
// it had before included) and certificate were its own; one behind a host shares them with that host's
|
|
275
|
+
// desktop at the same address, so they stay.
|
|
231
276
|
if (!via) {
|
|
232
|
-
(
|
|
233
|
-
this.injector.get(desktop_service_1.RemoteDesktopService).forgetCertificatesFor(id, true);
|
|
277
|
+
this.injector.get(desktop_service_1.RemoteDesktopService).forgetDesktop(id, true, `"${this.profile.name}"`).catch(() => null);
|
|
234
278
|
}
|
|
235
279
|
return;
|
|
236
280
|
}
|
|
281
|
+
// Its saved password goes with its way there (see desktopEdited): another gateway is asked for again.
|
|
237
282
|
const accountChanged = `${o.username ?? ''}\n${o.domain ?? ''}\n${o.account ?? ''}` !== account;
|
|
238
|
-
this.injector.get(desktop_service_1.RemoteDesktopService).desktopEdited(id, (0, desktops_1.desktopIdOf)(o), accountChanged, !via).catch(() => null);
|
|
283
|
+
this.injector.get(desktop_service_1.RemoteDesktopService).desktopEdited(id, (0, desktops_1.desktopIdOf)(o), accountChanged, !via, { from: gateway, to: o.gateway }).catch(() => null);
|
|
239
284
|
}
|
|
240
285
|
};
|
|
241
286
|
exports.RDPProfileSettingsComponent = RDPProfileSettingsComponent;
|
|
@@ -257,17 +302,20 @@ let RDPProfilesService = class RDPProfilesService extends tabby_core_1.QuickConn
|
|
|
257
302
|
this.name = 'Remote desktop (RDP)';
|
|
258
303
|
this.settingsComponent = RDPProfileSettingsComponent;
|
|
259
304
|
this.configDefaults = {
|
|
260
|
-
options: { host: '', port: 3389, kind: 'windows', username: '', domain: '', account: '', via: '' },
|
|
305
|
+
options: { host: '', port: 3389, kind: 'windows', username: '', domain: '', account: '', via: '', gateway: '', gatewayAccount: '', clipboard: '' },
|
|
261
306
|
clearServiceMessagesOnConnect: false,
|
|
262
307
|
};
|
|
263
308
|
}
|
|
264
309
|
async getBuiltinProfiles() {
|
|
310
|
+
// No clipboard here: a new profile is made from a copy of this, and a value saved with it (even '', the
|
|
311
|
+
// setting's) would hide the clipboard its group's or the type's defaults give it, which may well be narrower.
|
|
312
|
+
// Left out, the profile follows those defaults (configDefaults has the key) until it is given one of its own.
|
|
265
313
|
return [{
|
|
266
314
|
id: `${targets_1.RDP_PROFILE_TYPE}:template`,
|
|
267
315
|
type: targets_1.RDP_PROFILE_TYPE,
|
|
268
316
|
name: 'Remote desktop (RDP)',
|
|
269
317
|
icon: 'fas fa-desktop',
|
|
270
|
-
options: { host: '', port: 3389, kind: 'windows', username: '', domain: '', account: '', via: '' },
|
|
318
|
+
options: { host: '', port: 3389, kind: 'windows', username: '', domain: '', account: '', via: '', gateway: '', gatewayAccount: '' },
|
|
271
319
|
isBuiltin: true,
|
|
272
320
|
isTemplate: true,
|
|
273
321
|
}];
|
|
@@ -291,12 +339,19 @@ let RDPProfilesService = class RDPProfilesService extends tabby_core_1.QuickConn
|
|
|
291
339
|
}
|
|
292
340
|
getDescription(profile) {
|
|
293
341
|
const o = profile.options ?? {};
|
|
294
|
-
|
|
342
|
+
// Each field as text whatever the config holds (see configText): Tabby asks this of every profile as it lists
|
|
343
|
+
// them in its profile selector, and one that throws keeps the selector from opening at all.
|
|
344
|
+
const host = (0, desktops_1.configText)(o.host);
|
|
345
|
+
if (!host) {
|
|
295
346
|
return '';
|
|
296
347
|
}
|
|
297
|
-
const address = (0, desktopForm_1.formatAddress)(
|
|
298
|
-
const
|
|
299
|
-
|
|
348
|
+
const address = (0, desktopForm_1.formatAddress)(host, (0, osd_1.configNumber)(o.port || 3389));
|
|
349
|
+
const gateway = (0, desktops_1.configText)(o.gateway);
|
|
350
|
+
const via = [
|
|
351
|
+
...gateway ? [`the gateway ${gateway}`] : [],
|
|
352
|
+
...o.via ? [(0, desktops_1.configText)((this.config.store.profiles ?? []).find((p) => p.id === o.via)?.name) || 'an SSH profile'] : [],
|
|
353
|
+
];
|
|
354
|
+
return via.length ? `${address} via ${via.join(', via ')}` : address;
|
|
300
355
|
}
|
|
301
356
|
/** `[user@]host[:port]`, typed in Tabby's profile selector. */
|
|
302
357
|
quickConnect(query) {
|
|
@@ -313,16 +368,15 @@ let RDPProfilesService = class RDPProfilesService extends tabby_core_1.QuickConn
|
|
|
313
368
|
}
|
|
314
369
|
intoQuickConnectString(profile) {
|
|
315
370
|
const o = profile.options;
|
|
316
|
-
|
|
371
|
+
// As text, as getDescription reads them: the profile selector asks this of every profile too.
|
|
372
|
+
const username = (0, desktops_1.configText)(o.username);
|
|
373
|
+
return o.via ? null : `${username ? `${username}@` : ''}${(0, desktopForm_1.formatAddress)((0, desktops_1.configText)(o.host), (0, osd_1.configNumber)(o.port || 3389))}`;
|
|
317
374
|
}
|
|
318
375
|
deleteProfile(profile) {
|
|
319
|
-
// A direct desktop's saved
|
|
320
|
-
// desktop at the same address.
|
|
376
|
+
// A direct desktop's saved passwords (through any gateway: one it went through before too) and certificate are
|
|
377
|
+
// its own; one behind a host shares them with that host's desktop at the same address.
|
|
321
378
|
if (!profile.options.via) {
|
|
322
|
-
|
|
323
|
-
(0, signin_1.forgetCredentials)(`${desktops_1.DIRECT_KEY}#${id}`);
|
|
324
|
-
this.injector.get(desktop_service_1.RemoteDesktopService).forgetCertificatesFor(id, true);
|
|
325
|
-
this.config.save();
|
|
379
|
+
this.injector.get(desktop_service_1.RemoteDesktopService).forgetDesktop((0, desktops_1.desktopIdOf)(profile.options), true, `"${profile.name}"`).catch(() => null);
|
|
326
380
|
}
|
|
327
381
|
}
|
|
328
382
|
};
|
|
@@ -392,4 +446,3 @@ exports.RDPProfileOpener = RDPProfileOpener = __decorate([
|
|
|
392
446
|
(0, core_1.Injectable)(),
|
|
393
447
|
__metadata("design:paramtypes", [desktop_service_1.RemoteDesktopService, core_1.NgZone])
|
|
394
448
|
], RDPProfileOpener);
|
|
395
|
-
//# sourceMappingURL=rdpProfile.js.map
|
package/dist/remoteSetup.js
CHANGED
|
@@ -3,14 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.prepareRemoteDesktop = prepareRemoteDesktop;
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
|
+
const desktops_1 = require("./desktops");
|
|
6
7
|
const rdcleanpath_1 = require("./rdcleanpath");
|
|
7
8
|
// Runs as the SSH user; needs no root. Prints exactly one `RD_OK port=N user=U pass=P cert=SHA256`,
|
|
8
9
|
// `RD_XRDP port=N user=U` (no GNOME, but xrdp), `RD_WINDOWS` (a Windows host's POSIX sh) or `RD_ERR <reason>` line;
|
|
9
|
-
// with RD_OK, also an RD_XRDP line when xrdp runs besides GNOME, `RD_CLIENTS N`, the clients connected already,
|
|
10
|
-
// `RD_TAKEN_OVER <client> <seconds ago>` after a recent take-over
|
|
10
|
+
// with RD_OK, also an RD_XRDP line when xrdp runs besides GNOME, `RD_CLIENTS N`, the clients connected already,
|
|
11
|
+
// `RD_TAKEN_OVER <client> <seconds ago>` after a recent take-over, `RD_DESK <key>` with 'desk' on, and an `RD_NOTE <text>`
|
|
12
|
+
// for each rc file it couldn't change.
|
|
11
13
|
// The RDP password is generated once per remote user and kept in a 0600 file; grd reads credentials only
|
|
12
14
|
// at startup, so grd is restarted only when its configuration actually changes (that drops live sessions).
|
|
13
|
-
//
|
|
15
|
+
// Secrets never go on a command line, which any user there can see in the process list, except where grdctl can't
|
|
16
|
+
// read the password from its input: then it is an argument once, when the credentials are set.
|
|
14
17
|
const SETUP_SCRIPT = String.raw `
|
|
15
18
|
say () { printf '%s\n' "$*"; }
|
|
16
19
|
fail () { say "RD_ERR $*"; exit 0; }
|
|
@@ -59,12 +62,49 @@ OLD="$HOME/.local/share/tabby-remote-desktop"
|
|
|
59
62
|
if [ -d "$OLD" ] && [ ! -e "$D" ]; then
|
|
60
63
|
mv "$OLD" "$D" || fail "could not move $OLD to $D"
|
|
61
64
|
fi
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
mkdir -p "$D" && chmod 700 "$D" || fail "could not create $D"
|
|
66
|
+
# The login hook ('desk'): this one line in ~/.bashrc and ~/.zshrc, added and removed exactly, so that a line of the
|
|
67
|
+
# user's own that only looks like it stays.
|
|
68
|
+
HOOK='[ -f "$HOME/.local/share/tabby-rdp/login.sh" ] && . "$HOME/.local/share/tabby-rdp/login.sh" # tabby-rdp'
|
|
69
|
+
# Takes the lines that are exactly $2 out of rc file $1 and changes nothing else. grep reads it byte by byte, as text
|
|
70
|
+
# (LC_ALL=C, -a): otherwise a NUL byte, or a byte the host's locale can't read, has it leave lines out or print "Binary
|
|
71
|
+
# file … matches" in their place. What it leaves must be the file less those lines, to the byte, and is written into
|
|
72
|
+
# the file itself, so that a symbolic link to it, another name for it (a hard link), its mode and its owner all stay.
|
|
73
|
+
# A file that can't be written (read-only) or doesn't add up is left as it is, and the setup says so (RD_NOTE).
|
|
74
|
+
unhook () {
|
|
75
|
+
[ -f "$1" ] || return 0
|
|
76
|
+
LC_ALL=C grep -a -q -xF -- "$2" "$1" 2>/dev/null
|
|
77
|
+
case $? in 0) ;; 1) return 0 ;; *) say "RD_NOTE desk's line may be in $1, which can't be read"; return 0 ;; esac
|
|
78
|
+
uh_tmp=$(mktemp "$D/rc.XXXXXX") || { say "RD_NOTE desk's line stays in $1: no temporary file could be made in $D"; return 0; }
|
|
79
|
+
LC_ALL=C grep -a -v -xF -- "$2" "$1" > "$uh_tmp"
|
|
80
|
+
# 1: no line left, the file held only the hook. Each line taken out goes with its newline, and grep ends a last line
|
|
81
|
+
# that has none with one: so what is left is shorter by exactly that, or one byte longer than that. A step that fails
|
|
82
|
+
# counts as a mismatch (rather than ending the script, as a failed sum would).
|
|
83
|
+
if [ $? -le 1 ] && uh_was=$(wc -c < "$1") && uh_lines=$(LC_ALL=C grep -a -c -xF -- "$2" "$1") && uh_now=$(wc -c < "$uh_tmp"); then
|
|
84
|
+
uh_more=$(( $uh_now - ($uh_was - $uh_lines * (${"$"}{#2} + 1)) ))
|
|
85
|
+
else
|
|
86
|
+
uh_more=-1
|
|
65
87
|
fi
|
|
88
|
+
if [ "$uh_more" != 0 ] && [ "$uh_more" != 1 ]; then
|
|
89
|
+
say "RD_NOTE desk's line stays in $1: the rest of it didn't copy exactly"
|
|
90
|
+
elif ! cat "$uh_tmp" 2>/dev/null > "$1"; then
|
|
91
|
+
# Not opened (read-only), the file is as it was, its line in it. Otherwise writing it failed on the way (a full
|
|
92
|
+
# disk): what it is to hold stays where the user can find it.
|
|
93
|
+
if ! LC_ALL=C grep -a -q -xF -- "$2" "$1" 2>/dev/null; then
|
|
94
|
+
say "RD_NOTE writing $1 failed; what it should hold (without desk's line) is in $uh_tmp"
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
say "RD_NOTE desk's line stays in $1: it can't be written (read-only?)"
|
|
98
|
+
fi
|
|
99
|
+
rm -f "$uh_tmp"
|
|
100
|
+
}
|
|
101
|
+
# The hook from before 0.2, as those versions wrote it, and what it sources, where the old folder is still there (it
|
|
102
|
+
# couldn't be moved over): so that a copy of the line that was changed since, and stays, does nothing either.
|
|
103
|
+
OLD_HOOK='[ -f "$HOME/.local/share/tabby-remote-desktop/login.sh" ] && . "$HOME/.local/share/tabby-remote-desktop/login.sh" # tabby-remote-desktop'
|
|
104
|
+
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
|
105
|
+
unhook "$rc" "$OLD_HOOK"
|
|
66
106
|
done
|
|
67
|
-
|
|
107
|
+
rm -f "$OLD/login.sh"
|
|
68
108
|
NEW_CERT=0
|
|
69
109
|
if [ ! -s "$D/tls.key" ]; then
|
|
70
110
|
openssl req -new -newkey rsa:3072 -days 3650 -nodes -x509 -subj "/CN=$(hostname)" \
|
|
@@ -77,8 +117,29 @@ CERT=$(openssl x509 -in "$D/tls.crt" -noout -fingerprint -sha256 2>/dev/null | s
|
|
|
77
117
|
# 'desk' support (setting, off by default): interactive SSH logins start inside a shareable session (trd-pty,
|
|
78
118
|
# or tmux), so 'desk' can attach this exact console to a terminal on the desktop. Rewritten on every connect;
|
|
79
119
|
# opt out per login with TABBY_NO_SESSION=1. When the setting is off, the hook and helpers are removed.
|
|
120
|
+
DESK_KEY=
|
|
80
121
|
if [ "${"$"}{TRD_DESK:-0}" = 1 ]; then
|
|
81
122
|
mkdir -p "$D/bin"
|
|
123
|
+
# 'desk' sends this key with each request, and the plugin (told it below) ignores requests without it: anything shown
|
|
124
|
+
# in a terminal could print the rest. Only this user can read it. Made once, like the RDP password: a key is a file of
|
|
125
|
+
# this user's own (not a link), private, holding 128 bits in hex.
|
|
126
|
+
desk_key () {
|
|
127
|
+
DESK_KEY=
|
|
128
|
+
[ -f "$D/desk-key" ] && [ ! -L "$D/desk-key" ] && [ -O "$D/desk-key" ] || return 1
|
|
129
|
+
DESK_KEY=$(cat "$D/desk-key")
|
|
130
|
+
case $DESK_KEY in *[!0-9a-f]*) DESK_KEY= ;; esac
|
|
131
|
+
[ ${"$"}{#DESK_KEY} = 32 ] || { DESK_KEY=; return 1; }
|
|
132
|
+
chmod 600 "$D/desk-key"
|
|
133
|
+
}
|
|
134
|
+
if ! desk_key; then
|
|
135
|
+
# Put in place in one step, where there is none: of two setups making one at once (two panes, or two computers),
|
|
136
|
+
# the first one's stays, and both report it.
|
|
137
|
+
T=$(mktemp "$D/desk-key.XXXXXX") && od -An -N16 -tx1 /dev/urandom | tr -d ' \n' > "$T" || fail "could not create a key for desk"
|
|
138
|
+
{ [ -e "$D/desk-key" ] || [ -L "$D/desk-key" ]; } && ! desk_key && rm -f "$D/desk-key"
|
|
139
|
+
ln "$T" "$D/desk-key" 2>/dev/null || desk_key || mv -f "$T" "$D/desk-key"
|
|
140
|
+
rm -f "$T"
|
|
141
|
+
desk_key || fail "could not create a key for desk"
|
|
142
|
+
fi
|
|
82
143
|
cat > "$D/login.sh" <<'TRD_EOF'
|
|
83
144
|
# tabby-rdp ('desk' setting): interactive SSH logins run inside a shareable session, so 'desk' in Tabby can
|
|
84
145
|
# bring this console to the remote desktop. Backend ("native" trd-pty or "tmux") is in $TRD_HOME/backend.
|
|
@@ -128,7 +189,9 @@ fi
|
|
|
128
189
|
# Which machine this is: a shared home folder puts this script on others too, and the tab may be connected to another
|
|
129
190
|
# machine than the one this runs on (ssh typed in its console).
|
|
130
191
|
MACHINE=$(cat /etc/machine-id 2>/dev/null || hostname)
|
|
131
|
-
|
|
192
|
+
# What Tabby knows a real request by: other output can't read this.
|
|
193
|
+
KEY=$(cat "$HOME/.local/share/tabby-rdp/desk-key" 2>/dev/null)
|
|
194
|
+
SEQ=$(printf '\033]7777;desk;%s;%s;%s;%s;%s;%s;%s\007' "$(b64 "$SESSION")" "$(b64 "$SOCKET")" "$(b64 "$PWD")" "$(b64 "$KIND")" "$(b64 "$MACHINE")" "$(b64 "$(hostname)")" "$(b64 "$KEY")")
|
|
132
195
|
if [ "$KIND" = tmux ]; then
|
|
133
196
|
printf '\033Ptmux;\033%s\033\\' "$SEQ" # tmux only lets it through as passthrough
|
|
134
197
|
else
|
|
@@ -143,19 +206,18 @@ chmod 755 "$D/bin/trd-pty"
|
|
|
143
206
|
printf '%s\n' "${"$"}{TRD_BACKEND:-native}" > "$D/backend"
|
|
144
207
|
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
|
145
208
|
[ -f "$rc" ] || continue
|
|
146
|
-
grep -
|
|
147
|
-
# Only one line: turning desk off deletes exactly it, so on/off cycles leave the file as it was.
|
|
148
|
-
|
|
149
|
-
|
|
209
|
+
LC_ALL=C grep -a -q -xF -- "$HOOK" "$rc" && continue
|
|
210
|
+
# Only one line: turning desk off deletes exactly it, so on/off cycles leave the file as it was. A line of its own:
|
|
211
|
+
# a newline first, unless the file is empty or its last byte is one (counted, as a NUL there reads as nothing).
|
|
212
|
+
{ [ ! -s "$rc" ] || [ $(( $(tail -c1 "$rc" | wc -l) )) = 1 ] || echo; printf '%s\n' "$HOOK"; } 2>/dev/null >> "$rc" \
|
|
213
|
+
|| say "RD_NOTE desk's line couldn't be added to $rc (read-only?): logins there don't start in a shared session"
|
|
150
214
|
done
|
|
151
215
|
else
|
|
152
216
|
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
|
153
|
-
|
|
154
|
-
sed -i.trd-bak '/# tabby-rdp$/d' "$rc" && rm -f "$rc.trd-bak"
|
|
155
|
-
fi
|
|
217
|
+
unhook "$rc" "$HOOK"
|
|
156
218
|
done
|
|
157
219
|
# Sessions already running keep going; new logins are plain again.
|
|
158
|
-
rm -f "$D/login.sh" "$D/tmux.conf" "$D/backend" "$D/bin/desk" "$D/bin/trd-pty"
|
|
220
|
+
rm -f "$D/login.sh" "$D/tmux.conf" "$D/backend" "$D/bin/desk" "$D/bin/trd-pty" "$D/desk-key"
|
|
159
221
|
fi
|
|
160
222
|
if [ ! -s "$D/rdp-password" ]; then
|
|
161
223
|
( umask 077; od -An -N18 -tx1 /dev/urandom | tr -d ' \n' > "$D/rdp-password" ) || fail "could not create credentials"
|
|
@@ -164,7 +226,11 @@ RD_USER=tabby
|
|
|
164
226
|
RD_PASS=$(cat "$D/rdp-password")
|
|
165
227
|
|
|
166
228
|
STATUS=$(grdctl --headless status --show-credentials 2>/dev/null)
|
|
167
|
-
|
|
229
|
+
TAB=$(printf '\t') NL='
|
|
230
|
+
'
|
|
231
|
+
# Whether grd's status has this line. Compared in the shell: as a command's argument, the password would show in the
|
|
232
|
+
# process list on every connect.
|
|
233
|
+
has () { case "$NL$STATUS$NL" in *"$NL$TAB$1$NL"*) return 0 ;; esac; return 1; }
|
|
168
234
|
g () { grdctl --headless rdp "$@" >/dev/null 2>&1; }
|
|
169
235
|
RESTART=0
|
|
170
236
|
# grd reads the certificate at startup: a new one at the same path needs a restart too, or grd would keep serving
|
|
@@ -176,7 +242,18 @@ fi
|
|
|
176
242
|
if ! has "View-only: no"; then g disable-view-only || fail "grdctl could not disable view-only"; RESTART=1; fi
|
|
177
243
|
if ! has "Status: enabled"; then g enable || fail "grdctl could not enable RDP"; RESTART=1; fi
|
|
178
244
|
if ! has "Username: $RD_USER" || ! has "Password: $RD_PASS"; then
|
|
179
|
-
|
|
245
|
+
# Left off the command line, the password stays out of the process list: grdctl versions that ask for it then read
|
|
246
|
+
# it from their input. Where that didn't take, it goes as an argument. Without timeout(1) on the host, grdctl runs
|
|
247
|
+
# as it is: its input is a pipe that ends after the password, so it doesn't wait for more.
|
|
248
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
249
|
+
printf '%s\n' "$RD_PASS" | timeout 10 grdctl --headless rdp set-credentials "$RD_USER" >/dev/null 2>&1
|
|
250
|
+
else
|
|
251
|
+
printf '%s\n' "$RD_PASS" | grdctl --headless rdp set-credentials "$RD_USER" >/dev/null 2>&1
|
|
252
|
+
fi
|
|
253
|
+
STATUS=$(grdctl --headless status --show-credentials 2>/dev/null)
|
|
254
|
+
if ! has "Username: $RD_USER" || ! has "Password: $RD_PASS"; then
|
|
255
|
+
g set-credentials "$RD_USER" "$RD_PASS" || fail "grdctl could not set credentials"
|
|
256
|
+
fi
|
|
180
257
|
RESTART=1
|
|
181
258
|
fi
|
|
182
259
|
|
|
@@ -309,6 +386,7 @@ if [ -s "$D/taken-over" ]; then
|
|
|
309
386
|
fi
|
|
310
387
|
# xrdp besides GNOME (on a port of its own; on grd's, one of them couldn't listen): offered as another desktop.
|
|
311
388
|
[ -z "$XRDP_PORT" ] || [ "$XRDP_PORT" = "$PORT" ] || say "RD_XRDP port=$XRDP_PORT user=$(id -un)"
|
|
389
|
+
[ -z "$DESK_KEY" ] || say "RD_DESK $DESK_KEY"
|
|
312
390
|
say "RD_OK port=$PORT user=$RD_USER pass=$RD_PASS cert=$CERT"
|
|
313
391
|
`;
|
|
314
392
|
let trdPtySource = null;
|
|
@@ -343,9 +421,10 @@ async function prepareRemoteDesktop(target, desk, backend = 'native', client = '
|
|
|
343
421
|
// it worth asking, so a Linux host costs nothing extra.
|
|
344
422
|
if (/^RD_WINDOWS$/m.test(out) || !/^RD_(OK|XRDP|ERR) /m.test(out) && await isWindows(target)) {
|
|
345
423
|
// Its RDP server makes its own certificate: trusted on first use, like a desktop behind the host.
|
|
346
|
-
return { kind: 'windows', port: 3389, username: target.key
|
|
424
|
+
return { kind: 'windows', port: 3389, username: (0, desktops_1.keyUser)(target.key), password: '', certificate: '' };
|
|
347
425
|
}
|
|
348
|
-
|
|
426
|
+
// The host's reason, as much of it as a status line shows: the host decides how long the line is.
|
|
427
|
+
const err = /^RD_ERR (.{0,1000})/m.exec(out);
|
|
349
428
|
if (err) {
|
|
350
429
|
throw new Error(err[1]);
|
|
351
430
|
}
|
|
@@ -354,13 +433,15 @@ async function prepareRemoteDesktop(target, desk, backend = 'native', client = '
|
|
|
354
433
|
if (ok) {
|
|
355
434
|
const certificate = (0, rdcleanpath_1.normalizeFingerprint)(ok[4]);
|
|
356
435
|
if (!certificate) {
|
|
357
|
-
|
|
436
|
+
// What the host put there, as long as it likes: as much of it as a fingerprint takes, for a status line.
|
|
437
|
+
throw new Error(`Remote setup reported no usable certificate fingerprint (${ok[4].slice(0, 100)})`);
|
|
358
438
|
}
|
|
359
439
|
const clients = Number(/^RD_CLIENTS (\d+)$/m.exec(out)?.[1] ?? 0);
|
|
360
440
|
const taken = /^RD_TAKEN_OVER (\S+) (\d+)$/m.exec(out);
|
|
361
441
|
return {
|
|
362
442
|
kind: 'gnome', port: Number(ok[1]), username: ok[2], password: ok[3], certificate, xrdpPort: xrdp ? Number(xrdp[1]) : undefined, xrdpUser: xrdp?.[2],
|
|
363
|
-
clients, takenOver: taken && taken[1] !== client ? Number(taken[2]) : undefined,
|
|
443
|
+
clients, takenOver: taken && taken[1] !== client ? Number(taken[2]) : undefined, deskKey: /^RD_DESK ([0-9a-f]{32})$/m.exec(out)?.[1],
|
|
444
|
+
notes: [...out.matchAll(/^RD_NOTE (.+)$/gm)].map(m => m[1]),
|
|
364
445
|
};
|
|
365
446
|
}
|
|
366
447
|
if (xrdp) {
|
|
@@ -369,4 +450,3 @@ async function prepareRemoteDesktop(target, desk, backend = 'native', client = '
|
|
|
369
450
|
}
|
|
370
451
|
throw new Error('Remote setup gave no result');
|
|
371
452
|
}
|
|
372
|
-
//# sourceMappingURL=remoteSetup.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionLog = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A line as a string of its own. V8 keeps a part cut out of a string (a slice, a pattern's match; 13 characters or
|
|
6
|
+
* more) as a view of the string it came from, and joining strings as a list of them: a short line made from a host's
|
|
7
|
+
* answer of megabytes would keep all of it in memory for as long as the line is kept. Joined to a character and cut
|
|
8
|
+
* from it again: V8 copies a joined string's characters into one string before it cuts a part out of it, so the line
|
|
9
|
+
* is a part of that copy, every character as it was, unpaired surrogates included. A copy through UTF-16 and back
|
|
10
|
+
* does the same at several times the cost, which every line logged pays.
|
|
11
|
+
*/
|
|
12
|
+
function copied(line) {
|
|
13
|
+
return (' ' + line).slice(1);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A desktop's connection log (DesktopSession.log): what the settings page's Copy log hands out. Much of it is of the
|
|
17
|
+
* remote's making (an RD Gateway's administrator's messages, a server's), which decides how many lines there are and
|
|
18
|
+
* when, for as long as the desktop stays open: kept whole, they could fill Tabby's memory and crash its window. So the
|
|
19
|
+
* log keeps the first lines (how the connection was set up) and the latest ones, with a line between them saying how
|
|
20
|
+
* many were left out, and each line is cut at MAX_LINE characters and kept as a copy of its own (see copied), which
|
|
21
|
+
* the text it was made from doesn't stay in memory with.
|
|
22
|
+
*/
|
|
23
|
+
class SessionLog extends Array {
|
|
24
|
+
constructor() {
|
|
25
|
+
super(...arguments);
|
|
26
|
+
/** How many lines were left out so far. */
|
|
27
|
+
this.dropped = 0;
|
|
28
|
+
}
|
|
29
|
+
// What is made from the log (filter, slice, map) is a plain array.
|
|
30
|
+
static get [Symbol.species]() {
|
|
31
|
+
return Array;
|
|
32
|
+
}
|
|
33
|
+
push(...lines) {
|
|
34
|
+
const { FIRST, LATEST, MAX_LINE } = SessionLog;
|
|
35
|
+
super.push(...lines.map(line => typeof line === 'string' ? copied(line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}…` : line) : line));
|
|
36
|
+
// Once the latest lines are twice as many as are kept, the older half of them goes in one step: each line then
|
|
37
|
+
// costs the same however many come.
|
|
38
|
+
const said = this.dropped ? 1 : 0;
|
|
39
|
+
const over = this.length - FIRST - said - LATEST;
|
|
40
|
+
if (over >= LATEST) {
|
|
41
|
+
this.dropped += over;
|
|
42
|
+
this.splice(FIRST, said + over, `(${this.dropped} lines left out here)`);
|
|
43
|
+
}
|
|
44
|
+
return this.length;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.SessionLog = SessionLog;
|
|
48
|
+
/** The first lines, kept whatever follows. */
|
|
49
|
+
SessionLog.FIRST = 200;
|
|
50
|
+
/** At least this many of the latest lines are kept (and at most twice as many, see push). */
|
|
51
|
+
SessionLog.LATEST = 2000;
|
|
52
|
+
/** The longest a line is kept. */
|
|
53
|
+
SessionLog.MAX_LINE = 4096;
|