-
-
Notifications
You must be signed in to change notification settings - Fork 63
Expand file tree
/
Copy pathgssapiKerberosProvider.js
More file actions
183 lines (164 loc) · 5.54 KB
/
Copy pathgssapiKerberosProvider.js
File metadata and controls
183 lines (164 loc) · 5.54 KB
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
// Minimal GSSAPI provider example using the `kerberos` (node-kerberos) package.
// Notes:
// - This is a starting point; RFC 1961 protection/MIC negotiation is noted as TODO.
// - Requires: npm install kerberos
// - Service principal (SPN) typically looks like: 'rcmd/your-hostname@YOUR.REALM'
import os from 'os';
// Reads a single RFC 1961-framed token from the socket using an internal buffer.
function createTokenIO(socket, firstChunk) {
let buffer = Buffer.from(firstChunk || []);
function append(chunk) {
buffer = buffer.length ? Buffer.concat([buffer, chunk]) : Buffer.from(chunk);
}
async function readBytes(n) {
while (buffer.length < n) {
const chunk = await new Promise((resolve, reject) => {
const onData = (data) => {
socket.off('error', onError);
socket.off('close', onClose);
resolve(data);
};
const onError = (err) => {
socket.off('data', onData);
socket.off('close', onClose);
reject(err);
};
const onClose = () => {
socket.off('data', onData);
socket.off('error', onError);
reject(new Error('socket closed while reading GSS token'));
};
socket.once('data', onData);
socket.once('error', onError);
socket.once('close', onClose);
});
append(chunk);
}
const out = buffer.slice(0, n);
buffer = buffer.slice(n);
return out;
}
async function readToken() {
const header = await readBytes(3);
const ver = header.readUInt8(0);
if (ver !== 0x01) throw new Error(`invalid GSSAPI version byte: ${ver}`);
const len = header.readUInt16BE(1);
if (len === 0) return Buffer.alloc(0);
const token = await readBytes(len);
return token;
}
function writeToken(token) {
const t = Buffer.from(token || []);
const out = Buffer.alloc(3 + t.length);
out.writeUInt8(0x01, 0);
out.writeUInt16BE(t.length, 1);
t.copy(out, 3);
socket.write(out);
}
return { readToken, writeToken };
}
function loadKerberos() {
let mod;
try {
mod = require('kerberos');
} catch (error) {
const err = new Error('kerberos module not found. Install with: npm install kerberos');
err.cause = error;
throw err;
}
// Support both ESM and CJS shapes
return mod.default || mod;
}
/**
* A minimal provider implementing the contract expected by src/socks5.js Phase 1.
*
* Options:
* - service: string, default 'rcmd'
* - hostname: string, default os.hostname()
* - principal: explicit SPN, e.g., 'rcmd/host@REALM' (overrides service+hostname)
* - keytab: optional, path to keytab (depends on kerberos module support)
*/
function createKerberosProvider(options = {}) {
const service = options.service || 'rcmd';
const hostname = options.hostname || os.hostname();
const principal = options.principal || `${service}/${hostname}`;
return {
async authenticate(socket, firstChunk, callback) {
let kerberos;
try {
kerberos = await loadKerberos();
} catch (e) {
callback(e);
return;
}
const { readToken, writeToken } = createTokenIO(socket, firstChunk);
// API compatibility: mongodb kerberos exposes initializeServer(principal, cb)
const initServer = kerberos.initializeServer || kerberos.KerberosServer
|| (kerberos.default && kerberos.default.initializeServer);
if (!initServer) {
callback(new Error('Unsupported kerberos module API: expected initializeServer'));
return;
}
try {
// Create server context
const server = await new Promise((resolve, reject) => {
if (typeof initServer === 'function' && initServer.length >= 2) {
// initializeServer(principal, cb)
initServer(principal, (err, srv) => (err ? reject(err) : resolve(srv)));
} else if (typeof kerberos.KerberosServer === 'function') {
// new KerberosServer(principal)
try {
resolve(new kerberos.KerberosServer(principal));
} catch (e) {
reject(e);
}
} else {
reject(new Error('Unable to construct kerberos server context'));
}
});
// Context token exchange loop
// Many implementations expose server.step(input, cb) -> output
const step = (input) =>
new Promise((resolve, reject) => {
if (typeof server.step === 'function') {
// Some APIs expect base64; try Buffer/string passthrough first
try {
server.step(input, (err, out) => (err ? reject(err) : resolve(out)));
} catch (e) {
reject(e);
}
} else {
reject(new Error('kerberos server context missing step()'));
}
});
// Receive first token from client
let inToken = await readToken();
// Step until no output token is produced (context established)
while (true) {
const outToken = await step(inToken);
if (outToken && outToken.length) {
writeToken(Buffer.isBuffer(outToken) ? outToken : Buffer.from(outToken));
}
// Read next token if client has more to send
// If the context is established, some implementations provide server.contextComplete
// We conservatively attempt to read another token and break on empty.
try {
inToken = await readToken();
if (!inToken || inToken.length === 0) break;
} catch {
// If no more tokens, assume context is established
break;
}
}
// TODO: RFC 1961 protection-level negotiation and MIC verification.
// Many clients will proceed with NONE. Some may require MIC; implement per
// your environment using server MIC methods.
const principalName = server.username || server.user || server.principal || principal;
callback(null, principalName);
} catch (err) {
callback(err);
}
},
};
}
export default createKerberosProvider;