Skip to content

Commit b3539e7

Browse files
committed
refactor: 重构 RPC 处理并集成 node-vlmcs
1 parent 0b85098 commit b3539e7

12 files changed

Lines changed: 718 additions & 290 deletions

File tree

binaries/vlmcs-darwin-arm64

-86.3 KB
Binary file not shown.

binaries/vlmcs-darwin-x64

-55.6 KB
Binary file not shown.

binaries/vlmcs-linux-arm64

-47.3 KB
Binary file not shown.

binaries/vlmcs-linux-x64

-53.1 KB
Binary file not shown.

node-vlmcs/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
"name": "node-vlmcs",
33
"version": "1.0.0",
44
"description": "Node.js implementation of vlmcs KMS client",
5-
"main": "dist/cli.js",
5+
"main": "dist/index.js",
6+
"exports": {
7+
".": {
8+
"types": "./dist/index.d.ts",
9+
"import": "./dist/index.js",
10+
"require": "./dist/index.js"
11+
},
12+
"./cli": "./dist/cli.js"
13+
},
14+
"types": "dist/index.d.ts",
615
"bin": {
716
"vlmcs": "dist/cli.js"
817
},

node-vlmcs/src/index.ts

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
import * as net from 'net'
2+
import { KmsData } from './data'
3+
import {
4+
guidToBuffer,
5+
stringToGuidLE,
6+
utf8ToUcs2,
7+
ucs2ToUtf8,
8+
unixTimeToFileTime,
9+
PID_BUFFER_SIZE,
10+
WORKSTATION_NAME_BUFFER,
11+
} from './types'
12+
import {
13+
createRequestV4,
14+
createRequestV6,
15+
decryptResponseV4,
16+
decryptResponseV6,
17+
} from './kms'
18+
import { rpcBindClient, rpcSendRequest } from './rpc'
19+
import { get16RandomBytes } from './crypto'
20+
import { connectToAddress } from './network'
21+
import { logRequestVerbose, logResponseVerbose } from './output'
22+
23+
const DEFAULT_TIMEOUT_MS = 5 * 1000
24+
25+
export interface VlmcsCheckParams {
26+
host: string
27+
port?: number
28+
protocol?: number
29+
edition?: number
30+
timeout?: number
31+
verbose?: boolean
32+
}
33+
34+
export interface VlmcsCheckResult {
35+
host: string
36+
content: string
37+
delay: number
38+
status: boolean
39+
}
40+
41+
function connectSocket(
42+
host: string,
43+
port: number,
44+
timeout: number,
45+
): Promise<net.Socket> {
46+
return new Promise((resolve, reject) => {
47+
const socket = net.createConnection({ host, port })
48+
49+
const cleanup = () => {
50+
socket.removeAllListeners('connect')
51+
socket.removeAllListeners('error')
52+
socket.removeAllListeners('timeout')
53+
}
54+
55+
socket.once('connect', () => {
56+
cleanup()
57+
socket.pause()
58+
resolve(socket)
59+
})
60+
61+
socket.once('error', err => {
62+
cleanup()
63+
socket.destroy()
64+
reject(err)
65+
})
66+
67+
socket.setTimeout(timeout, () => {
68+
cleanup()
69+
socket.destroy()
70+
reject(new Error(`Connection timeout after ${timeout}ms`))
71+
})
72+
})
73+
}
74+
75+
function normalizeEdition(edition: number): number {
76+
const raw = Number(edition)
77+
if (!Number.isFinite(raw)) return 1
78+
const normalized = Math.floor(raw)
79+
if (normalized < 1 || normalized > KmsData.skuItems.length) return 1
80+
return normalized
81+
}
82+
83+
function normalizeProtocol(
84+
protocol: number | undefined,
85+
fallback: number,
86+
): number {
87+
const raw = Number(protocol)
88+
if (!Number.isFinite(raw)) return fallback
89+
const normalized = Math.floor(raw)
90+
if (normalized < 4 || normalized > 6) return fallback
91+
return normalized
92+
}
93+
94+
function randomWorkstationName(): string {
95+
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
96+
const size = Math.floor(Math.random() * 12) + 6
97+
let name = 'KMS-'
98+
for (let i = 0; i < size; i++) {
99+
name += alphabet[Math.floor(Math.random() * alphabet.length)]
100+
}
101+
return name.slice(0, 63)
102+
}
103+
104+
function buildRequest({
105+
protocol,
106+
edition,
107+
}: {
108+
protocol: number
109+
edition: number
110+
}): Buffer {
111+
const sku = KmsData.skuItems[edition - 1]
112+
const kms = KmsData.kmsItems[sku.kmsIndex]
113+
const app = KmsData.appItems[sku.appIndex]
114+
115+
const request = Buffer.alloc(236)
116+
request.writeUInt16LE(0, 0)
117+
request.writeUInt16LE(protocol, 2)
118+
request.writeUInt32LE(0, 4)
119+
request.writeUInt32LE(0x02, 8)
120+
request.writeUInt32LE(43200, 12)
121+
122+
guidToBuffer(stringToGuidLE(app.guid)!).copy(request, 16)
123+
guidToBuffer(stringToGuidLE(sku.guid)!).copy(request, 32)
124+
guidToBuffer(stringToGuidLE(kms.guid)!).copy(request, 48)
125+
126+
const cmid = get16RandomBytes()
127+
cmid[8] = (cmid[8] & 0x3f) | 0x80
128+
const d3 = cmid.readUInt16LE(6)
129+
cmid.writeUInt16LE((d3 & 0x0fff) | 0x4000, 6)
130+
cmid.copy(request, 64)
131+
132+
request.writeUInt32LE(sku.nCountPolicy, 80)
133+
unixTimeToFileTime().copy(request, 84)
134+
request.fill(0, 92, 108)
135+
136+
utf8ToUcs2(randomWorkstationName(), WORKSTATION_NAME_BUFFER).copy(
137+
request,
138+
108,
139+
)
140+
return request
141+
}
142+
143+
interface ParsedKmsResult {
144+
status: boolean
145+
summary: string
146+
ePID: string
147+
hwid: Buffer
148+
response: any
149+
}
150+
151+
function parseKmsResponse(
152+
rawResponse: Buffer,
153+
rawRequest: Buffer,
154+
): ParsedKmsResult {
155+
const responseMajorVersion = rawResponse.readUInt16LE(2)
156+
157+
if (responseMajorVersion === 4) {
158+
const { response, result } = decryptResponseV4(rawResponse, rawRequest)
159+
if (!result.decryptSuccess || !result.hashOK || !result.versionOK) {
160+
return {
161+
summary: 'Invalid KMS V4 response',
162+
status: false,
163+
ePID: '',
164+
hwid: Buffer.alloc(8),
165+
response,
166+
}
167+
}
168+
const ePID = ucs2ToUtf8(response.kmsPID, PID_BUFFER_SIZE)
169+
return {
170+
summary: ePID,
171+
status: true,
172+
ePID,
173+
hwid: Buffer.alloc(8),
174+
response,
175+
}
176+
}
177+
178+
const { response, result, hwid } = decryptResponseV6(rawResponse, rawRequest)
179+
if (!result.decryptSuccess || !result.hashOK || !result.versionOK) {
180+
return {
181+
summary: 'Invalid KMS V5/V6 response',
182+
status: false,
183+
ePID: '',
184+
hwid,
185+
response,
186+
}
187+
}
188+
189+
const ePID = ucs2ToUtf8(response.kmsPID, PID_BUFFER_SIZE)
190+
if (response.majorVer > 5) {
191+
const hwidHex = Array.from(hwid.subarray(0, 8))
192+
.map(b => b.toString(16).toUpperCase().padStart(2, '0'))
193+
.join('')
194+
return {
195+
summary: `${ePID} (${hwidHex})`,
196+
status: true,
197+
ePID,
198+
hwid,
199+
response,
200+
}
201+
}
202+
203+
return {
204+
summary: ePID,
205+
status: true,
206+
ePID,
207+
hwid,
208+
response,
209+
}
210+
}
211+
212+
function chunkToString(chunk: unknown): string {
213+
if (typeof chunk === 'string') return chunk
214+
if (Buffer.isBuffer(chunk)) return chunk.toString('utf8')
215+
return String(chunk)
216+
}
217+
218+
async function captureVerboseOutput<T>(fn: () => Promise<T>): Promise<{
219+
result: T
220+
output: string
221+
}> {
222+
let output = ''
223+
224+
const originalStdoutWrite = process.stdout.write
225+
const originalStderrWrite = process.stderr.write
226+
const originalConsoleLog = console.log
227+
const originalConsoleError = console.error
228+
229+
;(process.stdout.write as any) = ((chunk: unknown, ...args: unknown[]) => {
230+
output += chunkToString(chunk)
231+
const callback = args.find(arg => typeof arg === 'function') as
232+
| ((error?: Error | null) => void)
233+
| undefined
234+
callback?.(null)
235+
return true
236+
}) as typeof process.stdout.write
237+
;(process.stderr.write as any) = ((chunk: unknown, ...args: unknown[]) => {
238+
output += chunkToString(chunk)
239+
const callback = args.find(arg => typeof arg === 'function') as
240+
| ((error?: Error | null) => void)
241+
| undefined
242+
callback?.(null)
243+
return true
244+
}) as typeof process.stderr.write
245+
246+
console.log = (...args: unknown[]) => {
247+
output += `${args.map(chunkToString).join(' ')}\n`
248+
}
249+
250+
console.error = (...args: unknown[]) => {
251+
output += `${args.map(chunkToString).join(' ')}\n`
252+
}
253+
254+
try {
255+
const result = await fn()
256+
return { result, output: output.trimEnd() }
257+
} finally {
258+
process.stdout.write = originalStdoutWrite
259+
process.stderr.write = originalStderrWrite
260+
console.log = originalConsoleLog
261+
console.error = originalConsoleError
262+
}
263+
}
264+
265+
export async function runVlmcs(
266+
params: VlmcsCheckParams,
267+
): Promise<VlmcsCheckResult> {
268+
const { host, timeout = DEFAULT_TIMEOUT_MS, verbose = false } = params
269+
const portRaw = Number(params.port ?? 1688)
270+
const port = Number.isFinite(portRaw) ? Math.floor(portRaw) : 1688
271+
const edition = normalizeEdition(Number(params.edition ?? 1))
272+
273+
const sku = KmsData.skuItems[edition - 1]
274+
const protocol = normalizeProtocol(params.protocol, sku.protocolVersion)
275+
276+
const startedAt = Date.now()
277+
let socket: net.Socket | null = null
278+
279+
const executeCheck = async (): Promise<VlmcsCheckResult> => {
280+
try {
281+
const requestBase = buildRequest({ protocol, edition })
282+
283+
if (verbose) {
284+
logRequestVerbose(requestBase)
285+
}
286+
287+
socket = verbose
288+
? await connectToAddress(`${host}:${port}`, 0, false)
289+
: await connectSocket(host, port, timeout)
290+
291+
if (verbose) {
292+
process.stdout.write('\nPerforming RPC bind ...\n')
293+
}
294+
295+
const bind = await rpcBindClient(socket, verbose, true, true, true)
296+
if (bind.status !== 0) {
297+
return {
298+
host,
299+
status: false,
300+
delay: -1,
301+
content: `RPC bind failed: ${bind.status}`,
302+
}
303+
}
304+
305+
if (verbose) {
306+
process.stdout.write('... successful\n')
307+
}
308+
309+
const request =
310+
protocol < 5
311+
? createRequestV4(requestBase)
312+
: createRequestV6(requestBase)
313+
314+
if (verbose) {
315+
process.stdout.write(
316+
`Sending activation request (KMS V${protocol}) 1 of 1 `,
317+
)
318+
}
319+
320+
const rpcResult = await rpcSendRequest(
321+
socket,
322+
request,
323+
bind.rpcFlags,
324+
true,
325+
false,
326+
)
327+
328+
if (rpcResult.status !== 0 || !rpcResult.kmsResponse) {
329+
return {
330+
host,
331+
status: false,
332+
delay: -1,
333+
content: `KMS request failed: 0x${(rpcResult.status >>> 0).toString(16).toUpperCase().padStart(8, '0')}`,
334+
}
335+
}
336+
337+
const parsed = parseKmsResponse(rpcResult.kmsResponse, request)
338+
339+
if (verbose && parsed.status) {
340+
logResponseVerbose(
341+
parsed.ePID,
342+
parsed.hwid,
343+
parsed.response,
344+
rpcResult.kmsResponse.length,
345+
)
346+
}
347+
348+
return {
349+
host,
350+
status: parsed.status,
351+
delay: parsed.status ? Date.now() - startedAt : -1,
352+
content: parsed.summary,
353+
}
354+
} catch (error) {
355+
return {
356+
host,
357+
status: false,
358+
delay: -1,
359+
content: error instanceof Error ? error.message : String(error),
360+
}
361+
} finally {
362+
if (socket) {
363+
socket.destroy()
364+
}
365+
}
366+
}
367+
368+
if (!verbose) {
369+
return executeCheck()
370+
}
371+
372+
const { result, output } = await captureVerboseOutput(executeCheck)
373+
return {
374+
...result,
375+
content: output || result.content,
376+
}
377+
}

0 commit comments

Comments
 (0)