Move file transfer restriction into network settings
This commit is contained in:
@@ -213,6 +213,7 @@ export class Accounts {
|
||||
this.policy(row.senderUserId!, userId) === 'block'
|
||||
)
|
||||
fail(409, 'REQUEST_STATE', '수락할 수 없는 요청입니다.');
|
||||
if (row.kind === 'file') this.service.admin.assertNewUploads();
|
||||
if (row.kind === 'file')
|
||||
this.service.storage.reserve({ id: row.senderUserId!, kind: 'user' }, id);
|
||||
if (allowSender) this.rule(userId, row.senderUserId!, 'allow');
|
||||
|
||||
@@ -224,12 +224,15 @@ export class Administration {
|
||||
if (this.blocked(req))
|
||||
fail(403, 'ACCESS_BLOCKED', '이 접속은 서비스 이용이 제한되어 있습니다.');
|
||||
}
|
||||
publicPolicy() {
|
||||
return { fileTransfersAllowed: !this.state.uploadsPaused, revision: this.state.revision };
|
||||
}
|
||||
assertNewUploads() {
|
||||
if (this.state.uploadsPaused)
|
||||
throw new ApiError(
|
||||
503,
|
||||
'UPLOADS_PAUSED',
|
||||
'새 파일 업로드를 잠시 중지했습니다. 나중에 다시 시도해 주세요.',
|
||||
'파일 전송이 일시 제한되어 있어요. 링크와 텍스트는 보낼 수 있어요.',
|
||||
);
|
||||
}
|
||||
admit(req, reply, actor) {
|
||||
@@ -322,7 +325,9 @@ export class Administration {
|
||||
)
|
||||
)
|
||||
this.automatic.resetCounters();
|
||||
const filePolicyChanged = next.uploadsPaused !== this.state.uploadsPaused;
|
||||
this.state = next;
|
||||
if (filePolicyChanged) this.service.io?.emit('service:policy', this.publicPolicy());
|
||||
this.compile();
|
||||
this.disconnectBlocked();
|
||||
return { ...this.state };
|
||||
|
||||
@@ -118,6 +118,7 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
|
||||
}
|
||||
});
|
||||
io.on('connection', (socket) => {
|
||||
socket.emit('service:policy', service.admin.publicPolicy());
|
||||
presence.connect((socket.data.guest as Guest).id, socket, socket.data.ip);
|
||||
if (socket.data.accountId) {
|
||||
socket.join(`account:${socket.data.accountId}`);
|
||||
@@ -478,6 +479,7 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
|
||||
device: service.device(guest.id),
|
||||
csrfToken: service.csrf(token!),
|
||||
expiresAt: guest.expiresAt,
|
||||
servicePolicy: service.admin.publicPolicy(),
|
||||
};
|
||||
});
|
||||
app.get('/api/device/snapshot', async (req) => {
|
||||
@@ -491,6 +493,7 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
|
||||
sameNetworkDiscoveryEnabled: cfg.discoveryEnabled,
|
||||
spaceInviteCodeLength: cfg.spaceInviteCodeLength,
|
||||
clipboardTextLimitBytes: cfg.maxTextBytes,
|
||||
servicePolicy: service.admin.publicPolicy(),
|
||||
locationDiscovery: {
|
||||
enabled: cfg.locationDiscoveryEnabled,
|
||||
radiusMeters: cfg.locationRadiusMeters,
|
||||
|
||||
@@ -588,7 +588,7 @@ export class Service {
|
||||
storageUserId?: string,
|
||||
) {
|
||||
this.cleanup();
|
||||
if (input.kind === 'file' && !storedSource) this.admin.assertNewUploads();
|
||||
if (input.kind === 'file') this.admin.assertNewUploads();
|
||||
const target = this.deviceGuest(input.targetDeviceId);
|
||||
if (
|
||||
target &&
|
||||
@@ -696,6 +696,7 @@ export class Service {
|
||||
if (row.recipientGuestId !== g.id) fail(403, 'FORBIDDEN', '수신 기기만 수락할 수 있습니다.');
|
||||
if (row.status !== 'pending' || row.expiresAt <= Date.now())
|
||||
fail(409, 'REQUEST_STATE', '수락할 수 없는 요청입니다.');
|
||||
if (row.kind === 'file') this.admin.assertNewUploads();
|
||||
const permission = this.locationAcceptances.get(id);
|
||||
const reserved =
|
||||
this.cfg.locationDiscoveryEnabled &&
|
||||
|
||||
@@ -274,7 +274,7 @@ test('resumable offsets and completion honor the accepted manifest after reducin
|
||||
'upload-offset': String(offset),
|
||||
});
|
||||
assert.equal((await put(0, bytes.subarray(0, CHUNK_BYTES))).statusCode, 200);
|
||||
await patch(f, admin, { perFileBytes: 1, accountBytes: 1 });
|
||||
await patch(f, admin, { perFileBytes: 1, accountBytes: 1, uploadsPaused: true });
|
||||
assert.equal(
|
||||
(await put(CHUNK_BYTES, bytes.subarray(CHUNK_BYTES))).statusCode,
|
||||
200,
|
||||
@@ -283,7 +283,7 @@ test('resumable offsets and completion honor the accepted manifest after reducin
|
||||
const completed = await f.call(user, `/uploads/resumable/${id}/complete`, 'POST', {});
|
||||
assert.equal(completed.statusCode, 200, completed.body);
|
||||
assert.equal((await f.call(user, '/account/storage')).json().usedBytes, bytes.length);
|
||||
assert.equal((await own(f, user)).json().code, 'TRANSFER_LIMIT');
|
||||
assert.equal((await own(f, user)).json().code, 'UPLOADS_PAUSED');
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
@@ -364,3 +364,119 @@ test('offline transfer reset removes only transfer/guest data and keeps accounts
|
||||
await rm(folder, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'file-only ban publishes a minimal live policy, blocks new uploads, stored-file forwarding and pending acceptance while allowing clipboard and accepted uploads',
|
||||
{ timeout: 20000 },
|
||||
async () => {
|
||||
const f = await fixture();
|
||||
try {
|
||||
const admin = await signed(f, 'hello@noname.company'),
|
||||
a = await f.create(),
|
||||
b = await f.create(),
|
||||
c = await f.create(),
|
||||
d = await f.create();
|
||||
await f.space(a, b);
|
||||
await f.space(a, admin);
|
||||
await f.space(c, d);
|
||||
const original = await own(f, admin);
|
||||
assert.equal(
|
||||
(await upload(f, admin, original.json().requestId, Buffer.from('12345678'))).statusCode,
|
||||
200,
|
||||
);
|
||||
const stored = (await f.call(admin, '/vault')).json().items[0];
|
||||
const pending = await request(f, a, b),
|
||||
accepted = await accept(f, d, await request(f, c, d));
|
||||
assert.equal(pending.statusCode, 200, pending.body);
|
||||
const event = new Promise((resolve) => a.socket.once('service:policy', resolve));
|
||||
const settings = await patch(f, admin, { uploadsPaused: true });
|
||||
assert.deepEqual(await event, { fileTransfersAllowed: false, revision: settings.revision });
|
||||
const snapshot = (await f.call(a, '/device/snapshot')).json();
|
||||
assert.deepEqual(snapshot.servicePolicy, {
|
||||
fileTransfersAllowed: false,
|
||||
revision: settings.revision,
|
||||
});
|
||||
assert.equal('blockedIps' in snapshot.servicePolicy, false);
|
||||
assert.equal((await request(f, a, b)).json().code, 'UPLOADS_PAUSED');
|
||||
assert.equal((await own(f, admin)).json().code, 'UPLOADS_PAUSED');
|
||||
assert.equal(
|
||||
(
|
||||
await f.call(
|
||||
b,
|
||||
`/transfer-requests/${pending.json().requestId}/accept`,
|
||||
'POST',
|
||||
{},
|
||||
randomUUID(),
|
||||
)
|
||||
).json().code,
|
||||
'UPLOADS_PAUSED',
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await f.call(
|
||||
admin,
|
||||
`/vault/${stored.accessId}/send`,
|
||||
'POST',
|
||||
{ targetDeviceId: b.deviceId },
|
||||
randomUUID(),
|
||||
)
|
||||
).json().code,
|
||||
'UPLOADS_PAUSED',
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await f.call(admin, `/access/${stored.accessId}/download`)).rawPayload,
|
||||
Buffer.from('12345678'),
|
||||
);
|
||||
assert.equal((await upload(f, c, accepted, Buffer.from('12345678'))).statusCode, 200);
|
||||
await f.call(
|
||||
a,
|
||||
`/transfer-requests/${pending.json().requestId}/cancel`,
|
||||
'POST',
|
||||
{},
|
||||
randomUUID(),
|
||||
);
|
||||
await f.complete(a, b, 'https://example.test/allowed');
|
||||
for (const body of [
|
||||
{ kind: 'url', urls: ['https://example.test/clipboard'] },
|
||||
{ kind: 'text', text: '한글 日本語 😀\n 공백 유지' },
|
||||
]) {
|
||||
const saved = await f.call(admin, '/vault', 'POST', body, randomUUID());
|
||||
assert.equal(saved.statusCode, 200, saved.body);
|
||||
const item = (await f.call(admin, '/vault'))
|
||||
.json()
|
||||
.items.find((item) => item.kind === body.kind);
|
||||
const forwarded = await f.call(
|
||||
admin,
|
||||
`/vault/${item.accessId}/send`,
|
||||
'POST',
|
||||
{ targetDeviceId: b.deviceId },
|
||||
randomUUID(),
|
||||
);
|
||||
assert.equal(forwarded.statusCode, 200, forwarded.body);
|
||||
const received = await f.call(
|
||||
b,
|
||||
`/transfer-requests/${forwarded.json().requestId}/accept`,
|
||||
'POST',
|
||||
{},
|
||||
randomUUID(),
|
||||
);
|
||||
assert.equal(received.statusCode, 200, received.body);
|
||||
}
|
||||
const reconnected = new Promise((resolve) => a.socket.once('service:policy', resolve));
|
||||
a.socket.disconnect();
|
||||
a.socket.connect();
|
||||
assert.deepEqual(await reconnected, {
|
||||
fileTransfersAllowed: false,
|
||||
revision: settings.revision,
|
||||
});
|
||||
await patch(f, admin, { uploadsPaused: false });
|
||||
assert.equal((await request(f, a, b)).statusCode, 200);
|
||||
assert.equal(
|
||||
(await f.call(a, '/device/snapshot')).json().servicePolicy.fileTransfersAllowed,
|
||||
true,
|
||||
);
|
||||
} finally {
|
||||
await f.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -13,6 +13,8 @@ test('shared display sizes use decimal KB below 1MB and switch only at MB/GB bou
|
||||
[1100000, '1.1MB'],
|
||||
[1000000000, '1.0GB'],
|
||||
[1700000000, '1.7GB'],
|
||||
[7687e9, '7,687.0GB'],
|
||||
[1234567e9, '1,234,567.0GB'],
|
||||
])
|
||||
assert.equal(formatSize(bytes), expected);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user