mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 14:21:25 +02:00
A browser-based roguelike synchronized with crypto block timing. A run is a race against the next block: reach the exit before the block lands, or the house wins. The same codebase runs the solo dungeon, the social Tavern, and operator-selected PvP and race rulesets, with separate free and paid leaderboards. Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md for the design, and docs/DEPLOY.md to operate an instance.
165 lines
6.6 KiB
JavaScript
165 lines
6.6 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const {
|
|
CANARY_HANDSHAKE_CONFIRM,
|
|
CANARY_HANDSHAKE_PROTOCOL,
|
|
computeProof,
|
|
createCanaryDatabaseIdentityHandler,
|
|
equalProof,
|
|
readDatabaseIdentity
|
|
} = require('../src/services/canaryDatabaseIdentity');
|
|
|
|
function protectedNonceFile(nonce = 'ab'.repeat(32)) {
|
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'wowngeon-canary-nonce-'));
|
|
const fileName = path.join(directory, 'nonce');
|
|
fs.writeFileSync(fileName, `${nonce}\n`, { mode: 0o600 });
|
|
return { directory, fileName, nonce };
|
|
}
|
|
|
|
function enabledEnv(fileName, overrides = {}) {
|
|
return {
|
|
NODE_ENV: 'production',
|
|
CRYPTO_TYPE: 'XMR',
|
|
MONERO_NETWORK: 'stagenet',
|
|
PORT: '3102',
|
|
DB_NAME: 'monerogue_canary_direct_e2e',
|
|
CANARY_EXPECT_DATABASE: 'monerogue_canary_direct_e2e',
|
|
CANARY_DATABASE_HANDSHAKE: CANARY_HANDSHAKE_CONFIRM,
|
|
CANARY_DATABASE_NONCE_FILE: fileName,
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
function responseRecorder() {
|
|
return {
|
|
statusCode: 200,
|
|
body: null,
|
|
status(code) { this.statusCode = code; return this; },
|
|
json(body) { this.body = body; return this; }
|
|
};
|
|
}
|
|
|
|
describe('isolated canary database identity endpoint', () => {
|
|
test('is absent by default and fails closed on partial or non-stagenet configuration', () => {
|
|
expect(createCanaryDatabaseIdentityHandler({ db: {}, env: {} })).toBeNull();
|
|
expect(() => createCanaryDatabaseIdentityHandler({
|
|
db: {}, env: { CANARY_EXPECT_DATABASE: 'monerogue_canary_direct_e2e' }
|
|
})).toThrow(/CANARY_DATABASE_HANDSHAKE/);
|
|
|
|
const fixture = protectedNonceFile();
|
|
try {
|
|
expect(() => createCanaryDatabaseIdentityHandler({
|
|
db: {}, env: enabledEnv(fixture.fileName, { MONERO_NETWORK: 'mainnet' })
|
|
})).toThrow(/XMR stagenet/);
|
|
expect(() => createCanaryDatabaseIdentityHandler({
|
|
db: {}, env: enabledEnv(fixture.fileName, { PORT: '3000' })
|
|
})).toThrow(/3102/);
|
|
expect(() => createCanaryDatabaseIdentityHandler({
|
|
db: {}, env: enabledEnv(fixture.fileName, { DB_NAME: 'monerogue' })
|
|
})).toThrow(/DB_NAME/);
|
|
|
|
fs.chmodSync(fixture.fileName, 0o644);
|
|
expect(() => createCanaryDatabaseIdentityHandler({
|
|
db: {}, env: enabledEnv(fixture.fileName)
|
|
})).toThrow(/group\/world/);
|
|
fs.chmodSync(fixture.fileName, 0o600);
|
|
const link = path.join(fixture.directory, 'nonce-link');
|
|
fs.symlinkSync(fixture.fileName, link);
|
|
expect(() => createCanaryDatabaseIdentityHandler({
|
|
db: {}, env: enabledEnv(link)
|
|
})).toThrow();
|
|
} finally {
|
|
fs.rmSync(fixture.directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('signs the application pool exact cluster, OID, database, and caller challenge', async () => {
|
|
const fixture = protectedNonceFile();
|
|
const identity = {
|
|
cluster_id: '7612345678901234567',
|
|
database_oid: '24591',
|
|
database_name: 'monerogue_canary_direct_e2e'
|
|
};
|
|
const db = { query: jest.fn().mockResolvedValue({ rows: [identity] }) };
|
|
try {
|
|
const handler = createCanaryDatabaseIdentityHandler({
|
|
db, env: enabledEnv(fixture.fileName)
|
|
});
|
|
const challenge = '12'.repeat(32);
|
|
const response = responseRecorder();
|
|
await handler({
|
|
get: () => challenge,
|
|
socket: { remoteAddress: '::ffff:127.0.0.1' }
|
|
}, response);
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.body).toEqual(expect.objectContaining({
|
|
protocol: CANARY_HANDSHAKE_PROTOCOL,
|
|
challenge,
|
|
database: {
|
|
clusterId: identity.cluster_id,
|
|
databaseOid: identity.database_oid,
|
|
databaseName: identity.database_name
|
|
}
|
|
}));
|
|
const expected = computeProof(fixture.nonce, response.body.database, challenge);
|
|
expect(equalProof(response.body.proof, expected)).toBe(true);
|
|
expect(JSON.stringify(response.body)).not.toContain(fixture.nonce);
|
|
expect(db.query.mock.calls[0][0]).toContain('pg_control_system()');
|
|
} finally {
|
|
fs.rmSync(fixture.directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('rejects malformed challenges and refuses an application pool on another database', async () => {
|
|
const fixture = protectedNonceFile();
|
|
try {
|
|
const wrongDb = { query: jest.fn().mockResolvedValue({ rows: [{
|
|
cluster_id: '7612345678901234567',
|
|
database_oid: '24592',
|
|
database_name: 'monerogue_canary_credits_e2e'
|
|
}] }) };
|
|
const handler = createCanaryDatabaseIdentityHandler({
|
|
db: wrongDb, env: enabledEnv(fixture.fileName)
|
|
});
|
|
const malformed = responseRecorder();
|
|
const remote = responseRecorder();
|
|
await handler({
|
|
get: () => '34'.repeat(32), socket: { remoteAddress: '192.0.2.10' }
|
|
}, remote);
|
|
expect(remote.statusCode).toBe(404);
|
|
expect(wrongDb.query).not.toHaveBeenCalled();
|
|
|
|
await handler({
|
|
get: () => 'not-a-challenge', socket: { remoteAddress: '127.0.0.1' }
|
|
}, malformed);
|
|
expect(malformed.statusCode).toBe(400);
|
|
expect(wrongDb.query).not.toHaveBeenCalled();
|
|
|
|
const mismatch = responseRecorder();
|
|
await handler({
|
|
get: () => '34'.repeat(32), socket: { remoteAddress: '::1' }
|
|
}, mismatch);
|
|
expect(mismatch.statusCode).toBe(503);
|
|
expect(mismatch.body).toEqual({ error: 'Canary database identity mismatch.' });
|
|
} finally {
|
|
fs.rmSync(fixture.directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('requires one unambiguous row from PostgreSQL identity functions', async () => {
|
|
await expect(readDatabaseIdentity({
|
|
query: jest.fn().mockResolvedValue({ rows: [] })
|
|
})).rejects.toThrow(/ambiguous/);
|
|
await expect(readDatabaseIdentity({
|
|
query: jest.fn().mockResolvedValue({ rows: [{
|
|
cluster_id: '', database_oid: '1', database_name: 'db'
|
|
}] })
|
|
})).rejects.toThrow(/cluster identity/);
|
|
});
|
|
});
|