fix: 禁止NapCat WebUI心跳激活未引导会话
This commit is contained in:
parent
c9cc0d5e3a
commit
0375fd4661
@ -773,7 +773,7 @@ if (older) {
|
||||
}
|
||||
```
|
||||
|
||||
The `heartbeat()` implementation must reject `revoked`, `expired`, `failed`, missing sessions, and sessions whose user/account index no longer points at the requested `sessionId`. `requireBootstrapSession()` accepts only non-terminal, non-expired, currently indexed sessions for one-time ticket bootstrap. `requireProxySession()` accepts only `active`, non-expired, currently indexed sessions.
|
||||
The `heartbeat()` implementation must extend active sessions only. It must reject `created`, `revoked`, `expired`, `failed`, missing sessions, and sessions whose user/account index no longer points at the requested `sessionId`. `requireBootstrapSession()` accepts only non-terminal, non-expired, currently indexed sessions for one-time ticket bootstrap. `markActive()` is the only method that may promote a `created` session to `active`. `requireProxySession()` accepts only `active`, non-expired, currently indexed sessions.
|
||||
|
||||
- [ ] **Step 7: Implement Redis store and ticket service**
|
||||
|
||||
|
||||
@ -302,7 +302,7 @@ pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api jest --runTestsByPath test/
|
||||
|
||||
- [ ] **Step 6:实现 session service**
|
||||
|
||||
创建 `NapcatWebuiGatewaySessionService`,包含 `create`、`markActive`、`heartbeat`、`revoke`、`requireBootstrapSession`、`requireProxySession`。每个方法必须有 JSDoc。`create()` 必须撤销同用户同账号旧 session。`requireBootstrapSession()` 只接受非终态、未过期且 user/account index 仍指向当前 `sessionId` 的 session;`requireProxySession()` 只接受 `active`、未过期且 index 仍指向当前 `sessionId` 的 session。
|
||||
创建 `NapcatWebuiGatewaySessionService`,包含 `create`、`markActive`、`heartbeat`、`revoke`、`requireBootstrapSession`、`requireProxySession`。每个方法必须有 JSDoc。`create()` 必须撤销同用户同账号旧 session。`heartbeat()` 只延长 active session,必须拒绝 `created`、终态、过期、缺失或 user/account index 不再指向当前 `sessionId` 的 session。`requireBootstrapSession()` 只接受非终态、未过期且 user/account index 仍指向当前 `sessionId` 的 session;只有 `markActive()` 可以把 `created` session 提升为 `active`;`requireProxySession()` 只接受 `active`、未过期且 index 仍指向当前 `sessionId` 的 session。
|
||||
|
||||
- [ ] **Step 7:实现 Redis store 和 ticket service**
|
||||
|
||||
|
||||
@ -86,13 +86,13 @@ export class NapcatWebuiGatewaySessionService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends an Admin-owned active Gateway session.
|
||||
* Extends an Admin-owned Gateway session that bootstrap already activated.
|
||||
* @param input - Session id plus Admin ownership and request evidence.
|
||||
* @returns Browser-safe lifecycle result.
|
||||
*/
|
||||
async heartbeat(input: NapcatWebuiGatewayLifecycleInput) {
|
||||
const adminUserId = this.requireLifecycleAdminUserId(input.adminUserId);
|
||||
const session = await this.requireUsableSession(input.sessionId);
|
||||
const session = await this.requireProxySession(input.sessionId);
|
||||
this.assertOwner(session, adminUserId);
|
||||
const now = this.config.now();
|
||||
const expiresAt = now + this.config.ttlMs();
|
||||
|
||||
@ -381,6 +381,8 @@ describe('NapcatWebuiGatewaySessionService', () => {
|
||||
);
|
||||
const session = await service.create(createSessionInput());
|
||||
|
||||
await service.markActive(session.sessionId);
|
||||
|
||||
await expect(
|
||||
service.heartbeat({
|
||||
adminUserId: 'admin-2',
|
||||
@ -395,6 +397,25 @@ describe('NapcatWebuiGatewaySessionService', () => {
|
||||
).rejects.toThrow('Gateway session owner mismatch');
|
||||
});
|
||||
|
||||
it('rejects heartbeat for created sessions without activating them', async () => {
|
||||
const store = new MemorySessionStore();
|
||||
const service = new NapcatWebuiGatewaySessionService(
|
||||
store,
|
||||
createConfig({ value: 1000 }) as never,
|
||||
);
|
||||
const session = await service.create(createSessionInput());
|
||||
|
||||
await expect(
|
||||
service.heartbeat({
|
||||
adminUserId: 'admin-1',
|
||||
sessionId: session.sessionId,
|
||||
}),
|
||||
).rejects.toThrow('Gateway session is not active');
|
||||
expect(await store.find(session.sessionId)).toMatchObject({
|
||||
status: 'created',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps created sessions out of proxy access until bootstrap marks them active', async () => {
|
||||
const store = new MemorySessionStore();
|
||||
const currentTime = { value: 1000 };
|
||||
@ -813,6 +834,11 @@ describe('InternalSessionController', () => {
|
||||
.send(createSessionInput())
|
||||
.expect(HttpStatus.CREATED);
|
||||
const sessionId = createResponse.body.sessionId;
|
||||
await store.update(sessionId, {
|
||||
activeAt: 1000,
|
||||
lastSeenAt: 1000,
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/internal/sessions/${sessionId}/heartbeat`)
|
||||
@ -851,6 +877,24 @@ describe('InternalSessionController', () => {
|
||||
.expect(HttpStatus.BAD_REQUEST);
|
||||
});
|
||||
|
||||
it('rejects heartbeat for created sessions with gone status', async () => {
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post('/internal/sessions')
|
||||
.set('x-kt-gateway-secret', INTERNAL_SECRET)
|
||||
.send(createSessionInput())
|
||||
.expect(HttpStatus.CREATED);
|
||||
const sessionId = createResponse.body.sessionId;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/internal/sessions/${sessionId}/heartbeat`)
|
||||
.set('x-kt-gateway-secret', INTERNAL_SECRET)
|
||||
.send({ adminUserId: 'admin-1' })
|
||||
.expect(HttpStatus.GONE);
|
||||
expect(await store.find(sessionId)).toMatchObject({
|
||||
status: 'created',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects invalid create-session payloads with bad request status', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post('/internal/sessions')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user