VM 복귀 후 예약턴 잠금 복구와 Windows 부팅 식별자 확인을 추가한다

This commit is contained in:
Hide_D committed 2026-11-12 09:16:04 +09:00
1 parent fd8145d6eb
commit f36bc2c8d7
24 files changed
+1048 -28

No files matched your search

@@ -0,0 +1,134 @@
import { createHmac, randomBytes } from 'node:crypto';
import { once } from 'node:events';
import { createServer } from 'node:http';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createHostBootMonitor, loadHostBootMonitor } from '../src/turn/hostBootMonitor.js';
const cleanups: Array<() => Promise<void>> = [];
afterEach(async () => {
await Promise.all(cleanups.splice(0).map((cleanup) => cleanup()));
});
const fixture = async () => {
const key = randomBytes(32);
let bootId = 'a'.repeat(64);
let replay: string | null = null;
let mode: 'valid' | 'replay' | 'forged' | 'oversized' | 'offline' | 'hang' = 'valid';
const requests: string[] = [];
const server = createServer((request, response) => {
const nonce = String(request.headers['x-sammo-challenge']);
requests.push(nonce);
expect(request.headers['x-sammo-signature']).toBe(
createHmac('sha256', key).update(`sammo-host-boot-request-v1\n${nonce}`).digest('hex')
);
if (mode === 'hang') return;
if (mode === 'offline') {
response.writeHead(503).end();
return;
}
if (mode === 'oversized') {
response.end('x'.repeat(4096));
return;
}
if (mode === 'replay') {
response.end(replay);
return;
}
const body = JSON.stringify({
version: 1,
challenge: nonce,
bootId,
signature:
mode === 'forged'
? '0'.repeat(64)
: createHmac('sha256', key)
.update(`sammo-host-boot-response-v1\n${nonce}\n${bootId}`)
.digest('hex'),
});
replay ??= body;
response.end(body);
});
server.listen(0, '127.0.0.1');
await once(server, 'listening');
cleanups.push(
() =>
new Promise<void>((resolve) => {
server.close(() => resolve());
server.closeAllConnections();
})
);
const address = server.address();
if (!address || typeof address === 'string') throw new Error('Missing fixture listener');
return {
key,
requests,
url: `http://127.0.0.1:${address.port}/v1/boot-id`,
setBoot: (value: string) => {
bootId = value;
},
setMode: (value: typeof mode) => {
mode = value;
},
};
};
describe('Windows boot observation', () => {
it('uses a stable baseline, survives outages, and latches a signed boot change', async () => {
const f = await fixture();
const monitor = createHostBootMonitor({ ...f, pollMs: 0 });
expect(await monitor.hasChanged()).toBe(false);
f.setMode('offline');
expect(await monitor.hasChanged()).toBe(false);
f.setBoot('b'.repeat(64));
f.setMode('valid');
expect(await monitor.hasChanged()).toBe(true);
f.setMode('offline');
expect(await monitor.hasChanged()).toBe(true);
});
it.each(['replay', 'forged', 'oversized', 'hang'] as const)(
'rejects %s without changing the baseline',
async (mode) => {
const f = await fixture();
const onUnavailable = vi.fn();
const monitor = createHostBootMonitor({ ...f, pollMs: 0, timeoutMs: 100, onUnavailable });
expect(await monitor.hasChanged()).toBe(false);
f.setBoot('b'.repeat(64));
f.setMode(mode);
expect(await monitor.hasChanged()).toBe(false);
expect(onUnavailable).toHaveBeenCalledOnce();
f.setMode('valid');
expect(await monitor.hasChanged()).toBe(true);
}
);
it('samples immediately after a wall-clock jump despite a frozen monotonic cache and coalesces requests', async () => {
const f = await fixture();
let wall = 10_000;
const monitor = createHostBootMonitor({ ...f, now: () => 0, wallNow: () => wall });
expect(await monitor.hasChanged()).toBe(false);
expect(await monitor.hasChanged()).toBe(false);
expect(f.requests).toHaveLength(1);
wall += 300_000;
f.setBoot('b'.repeat(64));
expect(await Promise.all([monitor.hasChanged(), monitor.hasChanged()])).toEqual([true, true]);
expect(f.requests).toHaveLength(2);
expect(new Set(f.requests).size).toBe(2);
});
it('does not block game startup when optional configuration or its key file is absent', async () => {
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
expect(await loadHostBootMonitor({})).toBeUndefined();
expect(await loadHostBootMonitor({ TURN_HOST_BOOT_URL: 'http://host/v1/boot-id' })).toBeUndefined();
expect(
await loadHostBootMonitor({
TURN_HOST_BOOT_URL: 'http://host/v1/boot-id',
TURN_HOST_BOOT_KEY_FILE: '/nonexistent-host-boot-key',
})
).toBeUndefined();
expect(() =>
createHostBootMonitor({ url: 'http://user:password@host/v1/boot-id', key: randomBytes(32) })
).toThrow('without credentials');
warning.mockRestore();
});
});