@johnhenry/wsh 0.18.0 → 0.19.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/README.md +123 -15
- package/package.json +1 -1
- package/src/client.mjs +233 -23
- package/src/host-key.mjs +97 -0
- package/src/index.d.ts +70 -2
- package/src/index.mjs +1 -0
- package/src/server/auth.mjs +57 -0
- package/src/server/connection.mjs +119 -7
- package/src/server/fs.mjs +46 -6
- package/src/server/host-key.mjs +66 -0
- package/src/server/index.d.ts +38 -0
- package/src/server/index.mjs +27 -4
package/src/server/index.d.ts
CHANGED
|
@@ -20,6 +20,36 @@ export interface WshServerAuthOptions {
|
|
|
20
20
|
authorizedKeys?: string | Uint8Array | Array<string | Uint8Array>;
|
|
21
21
|
/** Extra policy, run after the signature verifies (and after the allowlist, when given). */
|
|
22
22
|
authorize?: (who: WshServerPrincipal) => boolean | Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Enable password login: return `true` to admit. Sent in the clear inside the
|
|
25
|
+
* connection -- serve over `wss://` / a trusted link. Use a constant-time
|
|
26
|
+
* compare against hashes, never `===` on plaintext. Failures are throttled
|
|
27
|
+
* (see `rateLimit`); the callback is not consulted while a caller is locked out.
|
|
28
|
+
*/
|
|
29
|
+
password?: (username: string, password: string) => boolean | Promise<boolean>;
|
|
30
|
+
/** Password-failure throttle. */
|
|
31
|
+
rateLimit?: {
|
|
32
|
+
/** Failures inside `windowMs` before lockout (default 5). */
|
|
33
|
+
maxFailures?: number;
|
|
34
|
+
/** Failure counting window (default 60000). */
|
|
35
|
+
windowMs?: number;
|
|
36
|
+
/** Lockout length once tripped (default 60000). */
|
|
37
|
+
lockoutMs?: number;
|
|
38
|
+
/** Pause before answering a failed attempt (default 250). */
|
|
39
|
+
failureDelayMs?: number;
|
|
40
|
+
/** Throttle bucket; default the peer address. Use this behind a proxy (e.g. read `x-forwarded-for` from `headers`). */
|
|
41
|
+
key?: (info: { address: string | undefined; headers: Record<string, string | string[] | undefined>; username: string }) => string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The server's own identity, as advertised to clients. */
|
|
46
|
+
export interface WshServerHostKey {
|
|
47
|
+
/** Hex SHA-256 of the raw key. */
|
|
48
|
+
fingerprint: string;
|
|
49
|
+
/** Raw 32-byte Ed25519 key. */
|
|
50
|
+
publicKey: Uint8Array;
|
|
51
|
+
/** `ssh-ed25519 AAAA...` */
|
|
52
|
+
openssh: string;
|
|
23
53
|
}
|
|
24
54
|
|
|
25
55
|
/** What a custom `exec` runner receives for one session. */
|
|
@@ -100,6 +130,12 @@ export interface WshServerOptions {
|
|
|
100
130
|
exec?: true | WshServerExecOptions | WshExecRunner;
|
|
101
131
|
/** Enable `pty` sessions by injecting a pty implementation. Off by default. */
|
|
102
132
|
pty?: WshServerPtyOptions;
|
|
133
|
+
/**
|
|
134
|
+
* Advertise an Ed25519 host identity (with proof of possession) so clients can pin it.
|
|
135
|
+
* `{ file }` persists a PKCS#8 PEM (created 0600 on first start) -- the normal choice;
|
|
136
|
+
* `true` is a fresh key every start; or pass a `CryptoKeyPair`. Off by default.
|
|
137
|
+
*/
|
|
138
|
+
hostKey?: true | { file: string } | CryptoKeyPair;
|
|
103
139
|
/** Enable file ops and uploads/downloads under one directory. Off by default. */
|
|
104
140
|
fs?: WshServerFsOptions;
|
|
105
141
|
/** How long exec output waits for a client that has not yet opened its data stream (default 3000). */
|
|
@@ -119,6 +155,8 @@ export interface WshServer {
|
|
|
119
155
|
close(): Promise<void>;
|
|
120
156
|
/** The bound address, or `null` when not listening. */
|
|
121
157
|
address(): WshServerAddress | null;
|
|
158
|
+
/** The advertised host identity; `null` before `listen()` resolves or without a `hostKey`. */
|
|
159
|
+
hostKey(): WshServerHostKey | null;
|
|
122
160
|
}
|
|
123
161
|
|
|
124
162
|
export function createWshServer(options?: WshServerOptions): WshServer;
|
package/src/server/index.mjs
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* refused.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { buildAuthorizer, parseAuthorizedKeys } from './auth.mjs';
|
|
11
|
+
import { buildAuthorizer, parseAuthorizedKeys, authMethods, FailureLimiter } from './auth.mjs';
|
|
12
|
+
import { loadHostKey } from './host-key.mjs';
|
|
12
13
|
import { spawnRunner } from './exec.mjs';
|
|
13
14
|
import { normalizePty } from './pty.mjs';
|
|
14
15
|
import { createFileAccess } from './fs.mjs';
|
|
@@ -25,12 +26,16 @@ export { parseAuthorizedKeys, STREAM_ANNOUNCE };
|
|
|
25
26
|
* `{ cwd, env, shell, timeoutMs, clientEnv }`, or a custom `run(command, io)` function.
|
|
26
27
|
* @param {{ spawn: Function, shell?: string, cwd?: string, env?: object }} [options.pty]
|
|
27
28
|
* @param {{ root: string, readOnly?: boolean, maxFileBytes?: number }} [options.fs]
|
|
29
|
+
* @param {true | { file: string } | CryptoKeyPair} [options.hostKey] - The server's own Ed25519
|
|
30
|
+
* identity, advertised (with proof of possession) so clients can pin it. See `src/host-key.mjs`.
|
|
31
|
+
* @param {object} [options.auth.rateLimit] - Password-failure throttle: `{ maxFailures=5, windowMs=60000,
|
|
32
|
+
* lockoutMs=60000, failureDelayMs=250, key(info) }` (`key` defaults to the peer address).
|
|
28
33
|
* @param {number} [options.bindTimeoutMs=3000]
|
|
29
34
|
* @param {(line: string) => void} [options.onLog]
|
|
30
35
|
* @returns {{ listen(): Promise<{address: string, port: number}>, close(): Promise<void>, address(): ({address: string, port: number} | null) }}
|
|
31
36
|
*/
|
|
32
37
|
export function createWshServer({
|
|
33
|
-
host = '127.0.0.1', port = 0, auth, exec, pty, fs, bindTimeoutMs = 3000, onLog = () => {},
|
|
38
|
+
host = '127.0.0.1', port = 0, auth, exec, pty, fs, hostKey, bindTimeoutMs = 3000, onLog = () => {},
|
|
34
39
|
} = {}) {
|
|
35
40
|
let wss = null;
|
|
36
41
|
let bound = null;
|
|
@@ -45,6 +50,14 @@ export function createWshServer({
|
|
|
45
50
|
}
|
|
46
51
|
const ptyConfig = pty ? normalizePty(pty) : null;
|
|
47
52
|
const files = fs ? createFileAccess(fs) : null;
|
|
53
|
+
const methods = authMethods(auth);
|
|
54
|
+
const rl = auth?.rateLimit ?? {};
|
|
55
|
+
const rateLimit = {
|
|
56
|
+
limiter: new FailureLimiter(rl),
|
|
57
|
+
key: typeof rl.key === 'function' ? rl.key : ({ address }) => String(address ?? 'unknown'),
|
|
58
|
+
failureDelayMs: rl.failureDelayMs ?? 250,
|
|
59
|
+
};
|
|
60
|
+
let host_ = null;
|
|
48
61
|
|
|
49
62
|
return {
|
|
50
63
|
async listen() {
|
|
@@ -56,13 +69,18 @@ export function createWshServer({
|
|
|
56
69
|
throw new Error('@johnhenry/wsh/server needs the "ws" package: npm install ws', { cause: err });
|
|
57
70
|
}
|
|
58
71
|
const authorize = await buildAuthorizer(auth);
|
|
72
|
+
host_ = hostKey ? await loadHostKey(hostKey) : null;
|
|
59
73
|
const attach = createConnectionFactory({
|
|
60
74
|
authorize, execRunner, execOptions, pty: ptyConfig, files, bindTimeoutMs, log: onLog,
|
|
75
|
+
methods, hostKey: host_, rateLimit,
|
|
61
76
|
});
|
|
62
77
|
|
|
63
78
|
wss = new WebSocketServer({ host, port });
|
|
64
|
-
wss.on('connection', (ws) => {
|
|
65
|
-
const conn = attach({
|
|
79
|
+
wss.on('connection', (ws, req) => {
|
|
80
|
+
const conn = attach({
|
|
81
|
+
send: (b) => { if (ws.readyState === 1) ws.send(b); },
|
|
82
|
+
remote: { address: req?.socket?.remoteAddress, headers: req?.headers ?? {} },
|
|
83
|
+
});
|
|
66
84
|
ws.on('message', (d) => {
|
|
67
85
|
const buf = Array.isArray(d) ? Buffer.concat(d) : Buffer.isBuffer(d) ? d : Buffer.from(d);
|
|
68
86
|
conn.receive(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
@@ -91,5 +109,10 @@ export function createWshServer({
|
|
|
91
109
|
address() {
|
|
92
110
|
return bound;
|
|
93
111
|
},
|
|
112
|
+
|
|
113
|
+
/** The advertised host identity (`null` until `listen()` resolves, or with no `hostKey`). */
|
|
114
|
+
hostKey() {
|
|
115
|
+
return host_ ? { fingerprint: host_.fingerprint, publicKey: host_.publicKey, openssh: host_.openssh } : null;
|
|
116
|
+
},
|
|
94
117
|
};
|
|
95
118
|
}
|