212 lines
7.7 KiB
JavaScript
212 lines
7.7 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
import { eq } from 'drizzle-orm';
|
|
import { fixture } from './helpers.js';
|
|
import { visitorRegion } from '../src/ownerInfo.js';
|
|
import { loadConfig } from '../src/config.js';
|
|
import * as s from '../src/db/schema.js';
|
|
|
|
test('basic country and city use only trusted Cloudflare headers and ignore precise coordinates', () => {
|
|
const cfg = { ...loadConfig(), clientIpSource: 'cloudflare', trustedProxies: ['127.0.0.1'] };
|
|
const req = {
|
|
raw: { socket: { remoteAddress: '127.0.0.1' } },
|
|
headers: {
|
|
'cf-connecting-ip': '8.8.8.8',
|
|
'cf-ipcountry': 'KR',
|
|
'cf-region': 'Daejeon',
|
|
'cf-ipcity': Buffer.from('대전', 'utf8').toString('latin1'),
|
|
'cf-iplatitude': '37.123456',
|
|
'cf-iplongitude': '127.123456',
|
|
},
|
|
};
|
|
assert.deepEqual(visitorRegion(req, cfg), {
|
|
ip: '8.8.8.8',
|
|
countryCode: 'KR',
|
|
region: 'Daejeon',
|
|
city: '대전',
|
|
source: 'cloudflare',
|
|
});
|
|
for (const change of [{ trustedProxies: [] }, { clientIpSource: 'forwarded' }])
|
|
assert.deepEqual(visitorRegion(req, { ...cfg, ...change }), {
|
|
ip: null,
|
|
countryCode: null,
|
|
region: null,
|
|
city: null,
|
|
source: 'unavailable',
|
|
});
|
|
assert.equal(
|
|
visitorRegion({ ...req, headers: { ...req.headers, 'cf-ipcountry': 'XX' } }, cfg).countryCode,
|
|
null,
|
|
);
|
|
});
|
|
test('region records are NAS-only, deduplicated by request, bounded and cleaned with owner files', async () => {
|
|
const f = await fixture();
|
|
try {
|
|
const a = await f.create(),
|
|
b = await f.create();
|
|
await f.space(a, b);
|
|
f.cfg.clientIpSource = 'cloudflare';
|
|
const key = randomUUID(),
|
|
input = {
|
|
targetDeviceId: b.deviceId,
|
|
kind: 'file',
|
|
byteSize: 4,
|
|
count: 1,
|
|
files: [{ name: 'test.mp4', size: 4, type: 'video/mp4' }],
|
|
},
|
|
headers = {
|
|
'cf-connecting-ip': '8.8.8.8',
|
|
'cf-ipcountry': 'KR',
|
|
'cf-region': 'Daejeon',
|
|
'cf-ipcity': 'Daejeon',
|
|
'cf-iplatitude': '37.123456',
|
|
};
|
|
const result = await f.call(a, '/transfer-requests', 'POST', input, key, headers);
|
|
assert.equal(result.statusCode, 200);
|
|
const id = result.json().requestId,
|
|
row = f.service.storage.fileRows(id)[0],
|
|
owner = f.service.storage.layout.ownerInfo.details(row.storageKey);
|
|
const json = () => JSON.parse(readFileSync(owner.path, 'utf8'));
|
|
assert.equal(json().uploader.id, row.ownerGuestId);
|
|
assert.equal(json().recentLocations[0].city, 'Daejeon');
|
|
const serialized = readFileSync(owner.path, 'utf8');
|
|
assert.equal(json().recentLocations[0].ip, '8.8.8.8');
|
|
for (const secret of ['37.123456', a.csrf, a.cookie])
|
|
assert.equal(serialized.includes(secret), false);
|
|
await f.call(a, '/transfer-requests', 'POST', input, key, headers);
|
|
assert.equal(json().recentLocations.length, 1);
|
|
for (let i = 0; i < 30; i++)
|
|
f.service.storage.layout.ownerInfo.record(row, randomUUID(), {
|
|
countryCode: 'CN',
|
|
region: null,
|
|
city: null,
|
|
source: 'cloudflare',
|
|
});
|
|
assert.equal(json().recentLocations.length, 20);
|
|
const info = f.service.storage.layout.ownerInfo;
|
|
assert.equal(
|
|
info.fileLocation(row).city,
|
|
'Daejeon',
|
|
'newer uploads cannot replace this file location',
|
|
);
|
|
info.record(row, id, { ip: '1.1.1.1', countryCode: 'JP', city: 'Tokyo' });
|
|
assert.equal(
|
|
info.fileLocation(row).city,
|
|
'Daejeon',
|
|
'idempotent retries cannot overwrite the original snapshot',
|
|
);
|
|
assert.equal(info.fileLocation(row).ip, '8.8.8.8');
|
|
const publicRequest = await f.call(b, '/transfer-requests');
|
|
assert.equal(publicRequest.body.includes('Daejeon'), false);
|
|
assert.equal(publicRequest.body.includes('8.8.8.8'), false);
|
|
const response = await f.app.inject({
|
|
url: '/uploads/originals/' + row.storageKey.split('/')[0] + '/owner-info.json',
|
|
});
|
|
assert.equal(
|
|
response.body.includes('recentLocations'),
|
|
false,
|
|
'metadata is not publicly served',
|
|
);
|
|
const expired = json();
|
|
for (const event of expired.recentLocations)
|
|
event.recordedAt = new Date(Date.now() - 8 * 86400000).toISOString();
|
|
writeFileSync(owner.path, JSON.stringify(expired));
|
|
await f.service.storage.layout.ownerInfo.cleanup();
|
|
assert.equal(json().recentLocations.length, 0);
|
|
f.service.db.update(s.files).set({ status: 'failed' }).where(eq(s.files.id, row.id)).run();
|
|
await f.service.storage.layout.ownerInfo.cleanup();
|
|
assert.equal(existsSync(owner.path), false);
|
|
assert.equal(f.service.db.select().from(s.fileUploadContexts).all().length, 0);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|
|
|
|
test('each file keeps its own request location; legacy lookup matches its request and expired snapshots are removed', async () => {
|
|
const f = await fixture();
|
|
try {
|
|
const a = await f.create(),
|
|
b = await f.create();
|
|
await f.space(a, b);
|
|
f.cfg.clientIpSource = 'cloudflare';
|
|
async function upload(country, city, ip) {
|
|
const request = await f.call(
|
|
a,
|
|
'/transfer-requests',
|
|
'POST',
|
|
{
|
|
targetDeviceId: b.deviceId,
|
|
kind: 'file',
|
|
count: 1,
|
|
byteSize: 4,
|
|
files: [{ name: 'test.txt', size: 4, type: 'text/plain' }],
|
|
},
|
|
randomUUID(),
|
|
{ 'cf-connecting-ip': ip, 'cf-ipcountry': country, 'cf-ipcity': city },
|
|
);
|
|
assert.equal(request.statusCode, 200, request.body);
|
|
const id = request.json().requestId;
|
|
await f.call(b, `/transfer-requests/${id}/accept`, 'POST', {}, randomUUID());
|
|
const result = await f.call(
|
|
a,
|
|
`/uploads?requestId=${id}&fileIndex=0`,
|
|
'POST',
|
|
Buffer.from(
|
|
'--loc\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\ntest\r\n--loc--\r\n',
|
|
),
|
|
randomUUID(),
|
|
{ 'content-type': 'multipart/form-data; boundary=loc' },
|
|
);
|
|
assert.equal(result.statusCode, 200, result.body);
|
|
return f.service.storage.fileRows(id)[0];
|
|
}
|
|
const first = await upload('KR', 'Daejeon', '8.8.8.8'),
|
|
second = await upload('JP', 'Tokyo', '1.1.1.1'),
|
|
info = f.service.storage.layout.ownerInfo;
|
|
const locations = new Map(
|
|
f.service.admin.files({}).items.map((file) => [file.id, file.lastLocation]),
|
|
);
|
|
assert.equal(locations.get(first.id).city, 'Daejeon');
|
|
assert.equal(locations.get(second.id).city, 'Tokyo');
|
|
f.service.db
|
|
.delete(s.fileUploadContexts)
|
|
.where(eq(s.fileUploadContexts.fileId, first.id))
|
|
.run();
|
|
assert.equal(
|
|
info.fileLocation(first).city,
|
|
'Daejeon',
|
|
'recover only the matching legacy request',
|
|
);
|
|
f.service.db
|
|
.delete(s.fileUploadContexts)
|
|
.where(eq(s.fileUploadContexts.fileId, first.id))
|
|
.run();
|
|
const owner = info.details(first.storageKey);
|
|
info.write(
|
|
owner,
|
|
info.read(owner).events.filter((event) => event.requestId !== first.requestId),
|
|
);
|
|
assert.equal(
|
|
info.fileLocation(first),
|
|
null,
|
|
'a missing old record must not use the newest location',
|
|
);
|
|
f.service.db
|
|
.update(s.fileUploadContexts)
|
|
.set({ expiresAt: Date.now() - 1 })
|
|
.run();
|
|
const old = info.read(owner).events.map((event) => ({
|
|
...event,
|
|
recordedAt: new Date(Date.now() - 8 * 86400000).toISOString(),
|
|
}));
|
|
info.write(owner, old);
|
|
await info.cleanup();
|
|
assert.equal(f.service.db.select().from(s.fileUploadContexts).all().length, 0);
|
|
assert.equal(info.fileLocation(second), null);
|
|
} finally {
|
|
await f.close();
|
|
}
|
|
});
|