@mt-tl/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/auth/handshake.d.ts +35 -0
- package/dist/auth/handshake.d.ts.map +1 -0
- package/dist/auth/handshake.js +208 -0
- package/dist/auth/handshake.js.map +1 -0
- package/dist/auth/nonce-store.d.ts +22 -0
- package/dist/auth/nonce-store.d.ts.map +1 -0
- package/dist/auth/nonce-store.js +23 -0
- package/dist/auth/nonce-store.js.map +1 -0
- package/dist/bootstrap.d.ts +36 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +82 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/config.d.ts +103 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -0
- package/dist/core/context.d.ts +57 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +27 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/errors.d.ts +33 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/rpc.d.ts +83 -0
- package/dist/core/rpc.d.ts.map +1 -0
- package/dist/core/rpc.js +102 -0
- package/dist/core/rpc.js.map +1 -0
- package/dist/core/updates.d.ts +56 -0
- package/dist/core/updates.d.ts.map +1 -0
- package/dist/core/updates.js +34 -0
- package/dist/core/updates.js.map +1 -0
- package/dist/create-server.d.ts +89 -0
- package/dist/create-server.d.ts.map +1 -0
- package/dist/create-server.js +109 -0
- package/dist/create-server.js.map +1 -0
- package/dist/crypto/aes-ige.d.ts +3 -0
- package/dist/crypto/aes-ige.d.ts.map +1 -0
- package/dist/crypto/aes-ige.js +55 -0
- package/dist/crypto/aes-ige.js.map +1 -0
- package/dist/crypto/dh.d.ts +21 -0
- package/dist/crypto/dh.d.ts.map +1 -0
- package/dist/crypto/dh.js +99 -0
- package/dist/crypto/dh.js.map +1 -0
- package/dist/crypto/hashes.d.ts +6 -0
- package/dist/crypto/hashes.d.ts.map +1 -0
- package/dist/crypto/hashes.js +14 -0
- package/dist/crypto/hashes.js.map +1 -0
- package/dist/crypto/msg-key.d.ts +15 -0
- package/dist/crypto/msg-key.d.ts.map +1 -0
- package/dist/crypto/msg-key.js +24 -0
- package/dist/crypto/msg-key.js.map +1 -0
- package/dist/crypto/rsa.d.ts +27 -0
- package/dist/crypto/rsa.d.ts.map +1 -0
- package/dist/crypto/rsa.js +50 -0
- package/dist/crypto/rsa.js.map +1 -0
- package/dist/dispatch/dispatcher.d.ts +72 -0
- package/dist/dispatch/dispatcher.d.ts.map +1 -0
- package/dist/dispatch/dispatcher.js +503 -0
- package/dist/dispatch/dispatcher.js.map +1 -0
- package/dist/dispatch/forwarders/in-process.d.ts +12 -0
- package/dist/dispatch/forwarders/in-process.d.ts.map +1 -0
- package/dist/dispatch/forwarders/in-process.js +15 -0
- package/dist/dispatch/forwarders/in-process.js.map +1 -0
- package/dist/dispatch/forwarders/print.d.ts +14 -0
- package/dist/dispatch/forwarders/print.d.ts.map +1 -0
- package/dist/dispatch/forwarders/print.js +23 -0
- package/dist/dispatch/forwarders/print.js.map +1 -0
- package/dist/dispatch/rpc-forwarder.d.ts +14 -0
- package/dist/dispatch/rpc-forwarder.d.ts.map +1 -0
- package/dist/dispatch/rpc-forwarder.js +2 -0
- package/dist/dispatch/rpc-forwarder.js.map +1 -0
- package/dist/dispatch/types.d.ts +32 -0
- package/dist/dispatch/types.d.ts.map +1 -0
- package/dist/dispatch/types.js +23 -0
- package/dist/dispatch/types.js.map +1 -0
- package/dist/gateway.d.ts +57 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +150 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +12 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +13 -0
- package/dist/lib.js.map +1 -0
- package/dist/server/message-pipeline.d.ts +57 -0
- package/dist/server/message-pipeline.d.ts.map +1 -0
- package/dist/server/message-pipeline.js +199 -0
- package/dist/server/message-pipeline.js.map +1 -0
- package/dist/session/inbound-tracker.d.ts +118 -0
- package/dist/session/inbound-tracker.d.ts.map +1 -0
- package/dist/session/inbound-tracker.js +170 -0
- package/dist/session/inbound-tracker.js.map +1 -0
- package/dist/session/message-id.d.ts +19 -0
- package/dist/session/message-id.d.ts.map +1 -0
- package/dist/session/message-id.js +30 -0
- package/dist/session/message-id.js.map +1 -0
- package/dist/session/salts.d.ts +51 -0
- package/dist/session/salts.d.ts.map +1 -0
- package/dist/session/salts.js +132 -0
- package/dist/session/salts.js.map +1 -0
- package/dist/session/session-manager.d.ts +18 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +43 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/storage/index.d.ts +14 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +16 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/memory.d.ts +3 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +91 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/mongo.d.ts +3 -0
- package/dist/storage/mongo.d.ts.map +1 -0
- package/dist/storage/mongo.js +175 -0
- package/dist/storage/mongo.js.map +1 -0
- package/dist/storage/types.d.ts +85 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +3 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/testkit.d.ts +11 -0
- package/dist/testkit.d.ts.map +1 -0
- package/dist/testkit.js +17 -0
- package/dist/testkit.js.map +1 -0
- package/dist/tl/codec.d.ts +37 -0
- package/dist/tl/codec.d.ts.map +1 -0
- package/dist/tl/codec.js +297 -0
- package/dist/tl/codec.js.map +1 -0
- package/dist/tl/layered-registry.d.ts +29 -0
- package/dist/tl/layered-registry.d.ts.map +1 -0
- package/dist/tl/layered-registry.js +118 -0
- package/dist/tl/layered-registry.js.map +1 -0
- package/dist/tl/protocol.d.ts +126 -0
- package/dist/tl/protocol.d.ts.map +1 -0
- package/dist/tl/protocol.js +22 -0
- package/dist/tl/protocol.js.map +1 -0
- package/dist/tl/reader.d.ts +30 -0
- package/dist/tl/reader.d.ts.map +1 -0
- package/dist/tl/reader.js +87 -0
- package/dist/tl/reader.js.map +1 -0
- package/dist/tl/registry.d.ts +39 -0
- package/dist/tl/registry.d.ts.map +1 -0
- package/dist/tl/registry.js +52 -0
- package/dist/tl/registry.js.map +1 -0
- package/dist/tl/writer.d.ts +24 -0
- package/dist/tl/writer.d.ts.map +1 -0
- package/dist/tl/writer.js +95 -0
- package/dist/tl/writer.js.map +1 -0
- package/dist/transport/connection-registry.d.ts +31 -0
- package/dist/transport/connection-registry.d.ts.map +1 -0
- package/dist/transport/connection-registry.js +84 -0
- package/dist/transport/connection-registry.js.map +1 -0
- package/dist/transport/connection.d.ts +62 -0
- package/dist/transport/connection.d.ts.map +1 -0
- package/dist/transport/connection.js +77 -0
- package/dist/transport/connection.js.map +1 -0
- package/dist/transport/framing.d.ts +39 -0
- package/dist/transport/framing.d.ts.map +1 -0
- package/dist/transport/framing.js +212 -0
- package/dist/transport/framing.js.map +1 -0
- package/dist/transport/proxy-protocol.d.ts +23 -0
- package/dist/transport/proxy-protocol.d.ts.map +1 -0
- package/dist/transport/proxy-protocol.js +108 -0
- package/dist/transport/proxy-protocol.js.map +1 -0
- package/dist/transport/server-common.d.ts +27 -0
- package/dist/transport/server-common.d.ts.map +1 -0
- package/dist/transport/server-common.js +27 -0
- package/dist/transport/server-common.js.map +1 -0
- package/dist/transport/tcp-server.d.ts +26 -0
- package/dist/transport/tcp-server.d.ts.map +1 -0
- package/dist/transport/tcp-server.js +91 -0
- package/dist/transport/tcp-server.js.map +1 -0
- package/dist/transport/ws-server.d.ts +19 -0
- package/dist/transport/ws-server.d.ts.map +1 -0
- package/dist/transport/ws-server.js +78 -0
- package/dist/transport/ws-server.js.map +1 -0
- package/dist/update-publisher.d.ts +34 -0
- package/dist/update-publisher.d.ts.map +1 -0
- package/dist/update-publisher.js +29 -0
- package/dist/update-publisher.js.map +1 -0
- package/dist/updates/mongo-update-log.d.ts +13 -0
- package/dist/updates/mongo-update-log.d.ts.map +1 -0
- package/dist/updates/mongo-update-log.js +39 -0
- package/dist/updates/mongo-update-log.js.map +1 -0
- package/dist/updates/presence-binder.d.ts +29 -0
- package/dist/updates/presence-binder.d.ts.map +1 -0
- package/dist/updates/presence-binder.js +36 -0
- package/dist/updates/presence-binder.js.map +1 -0
- package/dist/updates/presence.d.ts +31 -0
- package/dist/updates/presence.d.ts.map +1 -0
- package/dist/updates/presence.js +44 -0
- package/dist/updates/presence.js.map +1 -0
- package/dist/updates/push.d.ts +25 -0
- package/dist/updates/push.d.ts.map +1 -0
- package/dist/updates/push.js +72 -0
- package/dist/updates/push.js.map +1 -0
- package/dist/updates/redis-bus.d.ts +45 -0
- package/dist/updates/redis-bus.d.ts.map +1 -0
- package/dist/updates/redis-bus.js +59 -0
- package/dist/updates/redis-bus.js.map +1 -0
- package/dist/updates/redis-presence.d.ts +43 -0
- package/dist/updates/redis-presence.d.ts.map +1 -0
- package/dist/updates/redis-presence.js +65 -0
- package/dist/updates/redis-presence.js.map +1 -0
- package/dist/updates/render.d.ts +16 -0
- package/dist/updates/render.d.ts.map +1 -0
- package/dist/updates/render.js +46 -0
- package/dist/updates/render.js.map +1 -0
- package/dist/updates/router.d.ts +27 -0
- package/dist/updates/router.d.ts.map +1 -0
- package/dist/updates/router.js +36 -0
- package/dist/updates/router.js.map +1 -0
- package/dist/updates/types.d.ts +23 -0
- package/dist/updates/types.d.ts.map +1 -0
- package/dist/updates/types.js +2 -0
- package/dist/updates/types.js.map +1 -0
- package/dist/updates/update-bus.d.ts +29 -0
- package/dist/updates/update-bus.d.ts.map +1 -0
- package/dist/updates/update-bus.js +24 -0
- package/dist/updates/update-bus.js.map +1 -0
- package/dist/util/bytes.d.ts +12 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +46 -0
- package/dist/util/bytes.js.map +1 -0
- package/package.json +84 -0
- package/src/auth/handshake.ts +262 -0
- package/src/auth/nonce-store.ts +39 -0
- package/src/bootstrap.ts +114 -0
- package/src/config.ts +103 -0
- package/src/core/context.ts +94 -0
- package/src/core/errors.ts +52 -0
- package/src/core/index.ts +4 -0
- package/src/core/rpc.ts +165 -0
- package/src/core/updates.ts +69 -0
- package/src/create-server.ts +181 -0
- package/src/crypto/aes-ige.ts +57 -0
- package/src/crypto/dh.ts +101 -0
- package/src/crypto/hashes.ts +17 -0
- package/src/crypto/msg-key.ts +29 -0
- package/src/crypto/rsa.ts +70 -0
- package/src/dispatch/dispatcher.ts +586 -0
- package/src/dispatch/forwarders/in-process.ts +14 -0
- package/src/dispatch/forwarders/print.ts +22 -0
- package/src/dispatch/rpc-forwarder.ts +15 -0
- package/src/dispatch/types.ts +60 -0
- package/src/gateway.ts +214 -0
- package/src/index.ts +53 -0
- package/src/lib.ts +24 -0
- package/src/server/message-pipeline.ts +256 -0
- package/src/session/inbound-tracker.ts +221 -0
- package/src/session/message-id.ts +43 -0
- package/src/session/salts.ts +162 -0
- package/src/session/session-manager.ts +66 -0
- package/src/storage/index.ts +26 -0
- package/src/storage/memory.ts +101 -0
- package/src/storage/mongo.ts +215 -0
- package/src/storage/types.ts +92 -0
- package/src/testkit.ts +19 -0
- package/src/tl/codec.ts +292 -0
- package/src/tl/layered-registry.ts +132 -0
- package/src/tl/protocol.ts +146 -0
- package/src/tl/reader.ts +99 -0
- package/src/tl/registry.ts +78 -0
- package/src/tl/writer.ts +104 -0
- package/src/transport/connection-registry.ts +91 -0
- package/src/transport/connection.ts +113 -0
- package/src/transport/framing.ts +223 -0
- package/src/transport/proxy-protocol.ts +109 -0
- package/src/transport/server-common.ts +49 -0
- package/src/transport/tcp-server.ts +102 -0
- package/src/transport/ws-server.ts +94 -0
- package/src/update-publisher.ts +47 -0
- package/src/updates/mongo-update-log.ts +49 -0
- package/src/updates/presence-binder.ts +51 -0
- package/src/updates/presence.ts +61 -0
- package/src/updates/push.ts +90 -0
- package/src/updates/redis-bus.ts +86 -0
- package/src/updates/redis-presence.ts +87 -0
- package/src/updates/render.ts +53 -0
- package/src/updates/router.ts +52 -0
- package/src/updates/types.ts +24 -0
- package/src/updates/update-bus.ts +49 -0
- package/src/util/bytes.ts +49 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { noopLogger, type Logger } from '@mt-tl/tl'
|
|
2
|
+
import type { RpcForwarder, RpcRequest, RpcResponse } from '../rpc-forwarder.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dev fallback forwarder, used when no app is registered: logs the JSON-RPC 2.0
|
|
6
|
+
* envelope the handler *would* receive (info level), then returns NOT_IMPLEMENTED.
|
|
7
|
+
* Wire a real app via `createServer(...).register(app)` to get an
|
|
8
|
+
* {@link InProcessForwarder}.
|
|
9
|
+
*/
|
|
10
|
+
export class PrintForwarder implements RpcForwarder {
|
|
11
|
+
constructor(private readonly logger: Logger = noopLogger) {}
|
|
12
|
+
|
|
13
|
+
async forward(req: RpcRequest): Promise<RpcResponse> {
|
|
14
|
+
this.logger.info('rpc.print', {
|
|
15
|
+
id: req.id,
|
|
16
|
+
method: req.method,
|
|
17
|
+
params: req.params,
|
|
18
|
+
context: req.context,
|
|
19
|
+
})
|
|
20
|
+
return { error: { code: 501, message: 'NOT_IMPLEMENTED' } }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The envelope types describe what the engine hands the handler layer.
|
|
2
|
+
export type { RpcContext, RpcRequest, RpcResponse, SessionEffect } from '@mt-tl/tl'
|
|
3
|
+
import type { RpcRequest, RpcResponse } from '@mt-tl/tl'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bridge from a decoded business TL method to its handler. The framework is
|
|
7
|
+
* in-process, so there are two implementations:
|
|
8
|
+
* - {@link InProcessForwarder} — calls the app's `dispatchRpc` directly (prod
|
|
9
|
+
* and tests); what `createServer(...).listen()` wires.
|
|
10
|
+
* - `PrintForwarder` — the dev fallback when no app is registered; logs the
|
|
11
|
+
* decoded request envelope and returns NOT_IMPLEMENTED.
|
|
12
|
+
*/
|
|
13
|
+
export interface RpcForwarder {
|
|
14
|
+
forward(req: RpcRequest): Promise<RpcResponse>
|
|
15
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Connection } from '../transport/connection.js'
|
|
2
|
+
import type { TlObject } from '@mt-tl/tl'
|
|
3
|
+
import type { AcceptResult } from '../session/inbound-tracker.js'
|
|
4
|
+
|
|
5
|
+
/** Per-message context extracted from the encrypted envelope. */
|
|
6
|
+
export interface MessageContext {
|
|
7
|
+
msgId: bigint
|
|
8
|
+
seqNo: number
|
|
9
|
+
sessionId: bigint
|
|
10
|
+
authKeyId: bigint
|
|
11
|
+
salt: bigint
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SendOptions {
|
|
15
|
+
/** Notification (msg_id ends in 3), not a direct response. */
|
|
16
|
+
isNotification?: boolean
|
|
17
|
+
/** Consumes a seqno slot and gets an odd seqno (default true). */
|
|
18
|
+
contentRelated?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Sends an encrypted message back to a connected client. */
|
|
22
|
+
export interface Responder {
|
|
23
|
+
sendEncrypted(conn: Connection, body: TlObject, opts?: SendOptions): void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reply to a rejected inbound message (the non-`ok` result of
|
|
28
|
+
* `InboundTracker.accept`): `bad_msg_notification` for a coded violation,
|
|
29
|
+
* `msg_detailed_info` for a duplicate whose answer is cached, or nothing for a
|
|
30
|
+
* benign silent drop. Shared by the pipeline (outer envelope) and the dispatcher
|
|
31
|
+
* (container-inner messages).
|
|
32
|
+
*/
|
|
33
|
+
export function replyToBadAccept(
|
|
34
|
+
responder: Responder,
|
|
35
|
+
conn: Connection,
|
|
36
|
+
result: Exclude<AcceptResult, { ok: true }>,
|
|
37
|
+
msgId: bigint,
|
|
38
|
+
seqNo: number,
|
|
39
|
+
): void {
|
|
40
|
+
if ('code' in result) {
|
|
41
|
+
responder.sendEncrypted(
|
|
42
|
+
conn,
|
|
43
|
+
{ _: 'bad_msg_notification', bad_msg_id: msgId, bad_msg_seqno: seqNo, error_code: result.code },
|
|
44
|
+
{ contentRelated: false },
|
|
45
|
+
)
|
|
46
|
+
} else if ('detailed' in result) {
|
|
47
|
+
responder.sendEncrypted(
|
|
48
|
+
conn,
|
|
49
|
+
{
|
|
50
|
+
_: 'msg_detailed_info',
|
|
51
|
+
msg_id: msgId,
|
|
52
|
+
answer_msg_id: result.detailed.answerMsgId,
|
|
53
|
+
bytes: result.detailed.bytes,
|
|
54
|
+
status: 0,
|
|
55
|
+
},
|
|
56
|
+
{ contentRelated: false },
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
// { drop: true } → no reply.
|
|
60
|
+
}
|
package/src/gateway.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { protocolSchemaDir } from '@mt-tl/tl'
|
|
2
|
+
import { loadSchema } from './tl/registry.js'
|
|
3
|
+
import { TlCodec } from './tl/codec.js'
|
|
4
|
+
import { loadLayeredRegistry } from './tl/layered-registry.js'
|
|
5
|
+
import { loadRsaKeyPair } from './crypto/rsa.js'
|
|
6
|
+
import { createStorage, type Storage } from './storage/index.js'
|
|
7
|
+
import { NonceStore } from './auth/nonce-store.js'
|
|
8
|
+
import { Handshake } from './auth/handshake.js'
|
|
9
|
+
import { SaltService } from './session/salts.js'
|
|
10
|
+
import { Dispatcher } from './dispatch/dispatcher.js'
|
|
11
|
+
import { PrintForwarder } from './dispatch/forwarders/print.js'
|
|
12
|
+
import type { RpcForwarder } from './dispatch/rpc-forwarder.js'
|
|
13
|
+
import { MessagePipeline } from './server/message-pipeline.js'
|
|
14
|
+
import { MtprotoWsServer } from './transport/ws-server.js'
|
|
15
|
+
import { MtprotoTcpServer } from './transport/tcp-server.js'
|
|
16
|
+
import { Connection } from './transport/connection.js'
|
|
17
|
+
import { ConnectionRegistry } from './transport/connection-registry.js'
|
|
18
|
+
import { NodePresenceBinder, NoopPresenceBinder, type PresenceBinder } from './updates/presence-binder.js'
|
|
19
|
+
import { PushService } from './updates/push.js'
|
|
20
|
+
import type { Presence } from './updates/presence.js'
|
|
21
|
+
import type { UpdateBus } from './updates/update-bus.js'
|
|
22
|
+
import type { UpdateLog } from './core/updates.js'
|
|
23
|
+
import type { MTProtoConfig } from './config.js'
|
|
24
|
+
import { createLogger, type Logger } from '@mt-tl/tl'
|
|
25
|
+
import type { MigrationRegistry } from '@mt-tl/tl'
|
|
26
|
+
import type { KeyObject } from 'node:crypto'
|
|
27
|
+
|
|
28
|
+
export interface Gateway {
|
|
29
|
+
/** Present when `config.wsPort` is set. */
|
|
30
|
+
wsServer?: MtprotoWsServer
|
|
31
|
+
/** Present when `config.tcpPort` is set. */
|
|
32
|
+
tcpServer?: MtprotoTcpServer
|
|
33
|
+
pipeline: MessagePipeline
|
|
34
|
+
storage: Storage
|
|
35
|
+
registry: ConnectionRegistry
|
|
36
|
+
nodeId: string
|
|
37
|
+
fingerprint: bigint
|
|
38
|
+
/** Gateway's RSA public key (clients encrypt pq_inner_data with it). */
|
|
39
|
+
publicKey: KeyObject
|
|
40
|
+
stats: { constructors: number; methods: number; crcMismatches: number; layers: number[] }
|
|
41
|
+
/** Start every configured carrier. */
|
|
42
|
+
listen(): Promise<void>
|
|
43
|
+
close(): Promise<void>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BuildOptions {
|
|
47
|
+
forwarder?: RpcForwarder
|
|
48
|
+
/** Structured logger for observability; defaults to env-configured. */
|
|
49
|
+
logger?: Logger
|
|
50
|
+
/** Enables server-push: presence map (Redis in prod, in-memory for tests). */
|
|
51
|
+
presence?: Presence
|
|
52
|
+
/** Update bus this node consumes routed deliveries from (Redis pub/sub in prod). */
|
|
53
|
+
bus?: UpdateBus
|
|
54
|
+
/** Per-predicate migration ladders (input up / output down). */
|
|
55
|
+
migrations?: MigrationRegistry
|
|
56
|
+
/** Durable pts log read by the engine to answer getState/getDifference when managed. */
|
|
57
|
+
updateLog?: UpdateLog
|
|
58
|
+
/** When true, the engine answers `updates.getState`/`getDifference` from `updateLog`. */
|
|
59
|
+
managedUpdates?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Wires the gateway from config: load schema -> codec, RSA key, storage,
|
|
64
|
+
* handshake, dispatcher, pipeline, and the WS carrier. Returns the assembled
|
|
65
|
+
* gateway; call `wsServer.listen()` to start accepting clients.
|
|
66
|
+
*/
|
|
67
|
+
export async function buildGateway(config: MTProtoConfig, opts: BuildOptions = {}): Promise<Gateway> {
|
|
68
|
+
const logger = opts.logger ?? createLogger({ name: config.nodeId })
|
|
69
|
+
|
|
70
|
+
// Merge the framework's protocol schema with the app's business schema — the
|
|
71
|
+
// consumer ships only business `.tl`; the protocol layer lives in @mt-tl/tl.
|
|
72
|
+
const { registry, constructors, methods, crcMismatches } = loadSchema([
|
|
73
|
+
protocolSchemaDir,
|
|
74
|
+
config.schemaDir,
|
|
75
|
+
])
|
|
76
|
+
const layeredAll = loadLayeredRegistry(config.schemaLayersDir)
|
|
77
|
+
const layered = layeredAll.hasLayers() ? layeredAll : undefined
|
|
78
|
+
// Decode-union: register every layer's constructor ids so older-layer clients
|
|
79
|
+
// decode by id (the name index keeps the newest, so encode is unaffected).
|
|
80
|
+
for (const def of layeredAll.allDefs()) registry.register(def)
|
|
81
|
+
const codec = new TlCodec(registry, layered)
|
|
82
|
+
const rsa = loadRsaKeyPair(config.rsaKeyPath)
|
|
83
|
+
const storage = await createStorage(config.storage)
|
|
84
|
+
const saltService = new SaltService(storage.salts)
|
|
85
|
+
|
|
86
|
+
const nonceStore = new NonceStore()
|
|
87
|
+
const handshake = new Handshake({
|
|
88
|
+
codec,
|
|
89
|
+
rsa,
|
|
90
|
+
storage,
|
|
91
|
+
saltService,
|
|
92
|
+
nonceStore,
|
|
93
|
+
defaultLayer: config.defaultLayer,
|
|
94
|
+
logger: logger.child({ scope: 'handshake' }),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const forwarder = opts.forwarder ?? new PrintForwarder(logger.child({ scope: 'rpc' }))
|
|
98
|
+
|
|
99
|
+
// Presence / server-push wiring (no-op unless a presence map is supplied).
|
|
100
|
+
const connRegistry = new ConnectionRegistry()
|
|
101
|
+
const binder: PresenceBinder = opts.presence
|
|
102
|
+
? new NodePresenceBinder(config.nodeId, connRegistry, opts.presence)
|
|
103
|
+
: new NoopPresenceBinder()
|
|
104
|
+
|
|
105
|
+
const migrations = opts.migrations
|
|
106
|
+
const pipeline = new MessagePipeline({
|
|
107
|
+
codec,
|
|
108
|
+
storage,
|
|
109
|
+
handshake,
|
|
110
|
+
saltService,
|
|
111
|
+
defaultLayer: config.defaultLayer,
|
|
112
|
+
binder,
|
|
113
|
+
disableMsgKeyCheck: config.disableMsgKeyCheck,
|
|
114
|
+
disableSeqNoCheck: config.disableSeqNoCheck,
|
|
115
|
+
logger: logger.child({ scope: 'mtproto' }),
|
|
116
|
+
})
|
|
117
|
+
pipeline.dispatcher = new Dispatcher({
|
|
118
|
+
codec,
|
|
119
|
+
registry,
|
|
120
|
+
storage,
|
|
121
|
+
saltService,
|
|
122
|
+
responder: pipeline,
|
|
123
|
+
forwarder,
|
|
124
|
+
binder,
|
|
125
|
+
migrations,
|
|
126
|
+
logger: logger.child({ scope: 'rpc' }),
|
|
127
|
+
disableSeqNoCheck: config.disableSeqNoCheck,
|
|
128
|
+
updateLog: opts.updateLog,
|
|
129
|
+
managedUpdates: opts.managedUpdates,
|
|
130
|
+
allowedApiIds: config.allowedApiIds,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// Consume routed updates addressed to this node and push to local sockets.
|
|
134
|
+
if (opts.bus && opts.presence) {
|
|
135
|
+
const push = new PushService(
|
|
136
|
+
connRegistry,
|
|
137
|
+
pipeline,
|
|
138
|
+
layered,
|
|
139
|
+
migrations,
|
|
140
|
+
logger.child({ scope: 'push' }),
|
|
141
|
+
)
|
|
142
|
+
opts.bus.subscribeNode(config.nodeId, msg => {
|
|
143
|
+
if (msg.authKeyId !== undefined) push.deliverToAuthKey(msg.authKeyId, msg.update)
|
|
144
|
+
else if (msg.subject !== undefined) push.deliver(msg.subject, msg.update)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Refresh presence TTL for locally-connected users + auth keys (heartbeat).
|
|
149
|
+
let heartbeat: ReturnType<typeof setInterval> | undefined
|
|
150
|
+
if (opts.presence) {
|
|
151
|
+
const presence = opts.presence
|
|
152
|
+
heartbeat = setInterval(
|
|
153
|
+
() => {
|
|
154
|
+
for (const subject of connRegistry.subjects())
|
|
155
|
+
void presence.add(subject, config.nodeId).catch(() => {})
|
|
156
|
+
for (const authKeyId of connRegistry.authKeys())
|
|
157
|
+
void presence.addAuthKey(authKeyId, config.nodeId).catch(() => {})
|
|
158
|
+
},
|
|
159
|
+
Math.max(5_000, Math.floor(config.updates.presenceTtlMs / 3)),
|
|
160
|
+
)
|
|
161
|
+
if (typeof heartbeat.unref === 'function') heartbeat.unref()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const handlers = {
|
|
165
|
+
onPacket: (packet: Buffer, conn: Connection) => pipeline.handlePacket(packet, conn),
|
|
166
|
+
onClose: (conn: Connection) => binder.unbind(conn),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const wsServer =
|
|
170
|
+
config.wsPort !== undefined
|
|
171
|
+
? new MtprotoWsServer(
|
|
172
|
+
{
|
|
173
|
+
port: config.wsPort,
|
|
174
|
+
defaultLayer: config.defaultLayer,
|
|
175
|
+
trustProxy: config.trustProxy,
|
|
176
|
+
logger,
|
|
177
|
+
},
|
|
178
|
+
handlers,
|
|
179
|
+
)
|
|
180
|
+
: undefined
|
|
181
|
+
const tcpServer =
|
|
182
|
+
config.tcpPort !== undefined
|
|
183
|
+
? new MtprotoTcpServer(
|
|
184
|
+
{
|
|
185
|
+
port: config.tcpPort,
|
|
186
|
+
defaultLayer: config.defaultLayer,
|
|
187
|
+
trustProxy: config.trustProxy,
|
|
188
|
+
logger,
|
|
189
|
+
},
|
|
190
|
+
handlers,
|
|
191
|
+
)
|
|
192
|
+
: undefined
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
wsServer,
|
|
196
|
+
tcpServer,
|
|
197
|
+
pipeline,
|
|
198
|
+
storage,
|
|
199
|
+
registry: connRegistry,
|
|
200
|
+
nodeId: config.nodeId,
|
|
201
|
+
fingerprint: rsa.fingerprint,
|
|
202
|
+
publicKey: rsa.publicKey,
|
|
203
|
+
stats: { constructors, methods, crcMismatches, layers: layeredAll.layerNumbers() },
|
|
204
|
+
async listen() {
|
|
205
|
+
await Promise.all([wsServer?.listen(), tcpServer?.listen()])
|
|
206
|
+
},
|
|
207
|
+
async close() {
|
|
208
|
+
if (heartbeat) clearInterval(heartbeat)
|
|
209
|
+
wsServer?.close()
|
|
210
|
+
tcpServer?.close()
|
|
211
|
+
await storage.close()
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @mt-tl/server — the consumer-facing framework facade. Install this (plus
|
|
2
|
+
// @mt-tl/tl for codegen) to build an MTProto server: define routes, listen.
|
|
3
|
+
// The protocol engine (transport/crypto/session/dispatch) and the handler layer
|
|
4
|
+
// (registry/dispatch/hooks/context/errors, under ./core) live in this same
|
|
5
|
+
// package; this file re-exports just the consumer surface — not the internal
|
|
6
|
+
// event/job/runner machinery.
|
|
7
|
+
|
|
8
|
+
export * from './create-server.js' // createServer, definePlugin, MtprotoServer, Plugin, MethodOpts/Handler
|
|
9
|
+
export * from './update-publisher.js' // createUpdatePublisher
|
|
10
|
+
|
|
11
|
+
// Handler surface (curated from ./core).
|
|
12
|
+
export {
|
|
13
|
+
// errors → rpc_error
|
|
14
|
+
AppError,
|
|
15
|
+
BadRequestError,
|
|
16
|
+
AuthRequiredError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
FloodWaitError,
|
|
19
|
+
InternalError,
|
|
20
|
+
// hooks
|
|
21
|
+
defineHook,
|
|
22
|
+
type Hook,
|
|
23
|
+
// dispatch + registry (for tests / advanced wiring)
|
|
24
|
+
RpcRegistry,
|
|
25
|
+
dispatchRpc,
|
|
26
|
+
type DispatchDeps,
|
|
27
|
+
// context + typing
|
|
28
|
+
type HandlerCtx,
|
|
29
|
+
type RpcMethodSpec,
|
|
30
|
+
type RpcMethodMap,
|
|
31
|
+
type RpcModule,
|
|
32
|
+
type UpdateEmitter,
|
|
33
|
+
} from './core/index.js'
|
|
34
|
+
|
|
35
|
+
// The system config object the server takes.
|
|
36
|
+
export type { MTProtoConfig } from './lib.js'
|
|
37
|
+
|
|
38
|
+
// Structured logger (re-exported from @mt-tl/tl) — build one with createLogger
|
|
39
|
+
// and pass it to createServer, or use the same factory in your app code for a
|
|
40
|
+
// unified log style. Handlers get a per-request child via `ctx.log`; the server
|
|
41
|
+
// exposes its root logger as `app.log`. See docs/guide/observability.md.
|
|
42
|
+
export {
|
|
43
|
+
createLogger,
|
|
44
|
+
noopLogger,
|
|
45
|
+
type Logger,
|
|
46
|
+
type LogLevel,
|
|
47
|
+
type LoggerOptions,
|
|
48
|
+
type Fields,
|
|
49
|
+
} from './lib.js'
|
|
50
|
+
|
|
51
|
+
// Schema-version migration ladders (input `up` / output `down`) — pass via
|
|
52
|
+
// createServer(config, { migrations }). See docs/guide/releasing-a-version.md.
|
|
53
|
+
export { MigrationRegistry, type MigrationRung } from '@mt-tl/tl'
|
package/src/lib.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The protocol ENGINE's internal API (transport/crypto/session/dispatch wiring).
|
|
2
|
+
// Consumers don't import this directly; they use `createServer()` (./create-server),
|
|
3
|
+
// which wraps it. Env-free and side-effect-free: the caller builds an MTProtoConfig.
|
|
4
|
+
export { bootstrap, type BootstrapOptions, type ForwardHandler, type UpdatePublish } from './bootstrap.js'
|
|
5
|
+
export { buildGateway, type Gateway, type BuildOptions } from './gateway.js'
|
|
6
|
+
export { type MTProtoConfig } from './config.js'
|
|
7
|
+
export { InProcessForwarder } from './dispatch/forwarders/in-process.js'
|
|
8
|
+
export {
|
|
9
|
+
createLogger,
|
|
10
|
+
noopLogger,
|
|
11
|
+
type Logger,
|
|
12
|
+
type LogLevel,
|
|
13
|
+
type LoggerOptions,
|
|
14
|
+
type Fields,
|
|
15
|
+
} from '@mt-tl/tl'
|
|
16
|
+
export type { RpcForwarder } from './dispatch/rpc-forwarder.js'
|
|
17
|
+
export type { RpcContext, RpcRequest, RpcResponse, SessionEffect } from '@mt-tl/tl'
|
|
18
|
+
export type { UpdateMessage, NodeDelivery } from './updates/types.js'
|
|
19
|
+
|
|
20
|
+
// Update-delivery adapters — the Redis pub/sub bus + presence behind the
|
|
21
|
+
// in-process push loop (multi-instance), and the router that fans updates to nodes.
|
|
22
|
+
export { createRedisPresence, type RedisPresenceHandle } from './updates/redis-presence.js'
|
|
23
|
+
export { createRedisUpdateBus, type RedisBusHandle } from './updates/redis-bus.js'
|
|
24
|
+
export { UpdateRouter, type RouterOptions } from './updates/router.js'
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import { noopLogger, type Logger } from '@mt-tl/tl'
|
|
3
|
+
import { TlReader } from '../tl/reader.js'
|
|
4
|
+
import { TlWriter } from '../tl/writer.js'
|
|
5
|
+
import type { TlCodec } from '../tl/codec.js'
|
|
6
|
+
import type { TlObject } from '@mt-tl/tl'
|
|
7
|
+
import type { Connection } from '../transport/connection.js'
|
|
8
|
+
import type { Storage } from '../storage/index.js'
|
|
9
|
+
import type { SaltService } from '../session/salts.js'
|
|
10
|
+
import { Handshake } from '../auth/handshake.js'
|
|
11
|
+
import { Dispatcher } from '../dispatch/dispatcher.js'
|
|
12
|
+
import { replyToBadAccept, type MessageContext, type Responder, type SendOptions } from '../dispatch/types.js'
|
|
13
|
+
import { ensureSession } from '../session/session-manager.js'
|
|
14
|
+
import { messageClass } from '../session/inbound-tracker.js'
|
|
15
|
+
import { igeDecrypt, igeEncrypt } from '../crypto/aes-ige.js'
|
|
16
|
+
import { generateMessageKey, computeMsgKey } from '../crypto/msg-key.js'
|
|
17
|
+
import { toBigIntLE, toBufferLE } from '../util/bytes.js'
|
|
18
|
+
import type { PresenceBinder } from '../updates/presence-binder.js'
|
|
19
|
+
import { NoopPresenceBinder } from '../updates/presence-binder.js'
|
|
20
|
+
|
|
21
|
+
export interface PipelineDeps {
|
|
22
|
+
codec: TlCodec
|
|
23
|
+
storage: Storage
|
|
24
|
+
handshake: Handshake
|
|
25
|
+
saltService: SaltService
|
|
26
|
+
defaultLayer: number
|
|
27
|
+
/** Presence/registry binder; defaults to a no-op (push disabled). */
|
|
28
|
+
binder?: PresenceBinder
|
|
29
|
+
/**
|
|
30
|
+
* Disable the inbound MTProto 2.0 `msg_key` integrity check.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ INSECURE — leave this `false` (the default). When `false`, every inbound
|
|
33
|
+
* encrypted message must carry a `msg_key` equal to the 2.0 recompute
|
|
34
|
+
* `SHA256(authKey[88:120] ‖ plaintext)[8:24]`, which authenticates the
|
|
35
|
+
* ciphertext (integrity + binding to the auth key). Setting it `true` makes the
|
|
36
|
+
* gateway accept any ciphertext that merely decrypts to a well-formed message,
|
|
37
|
+
* dropping that authentication — only enable it as a temporary interop shim for
|
|
38
|
+
* non-compliant clients (e.g. ones still on the MTProto 1.0 msg_key scheme; see
|
|
39
|
+
* docs/internals/msgkey-v1-quirk.md).
|
|
40
|
+
*/
|
|
41
|
+
disableMsgKeyCheck?: boolean
|
|
42
|
+
/** Disable inbound seqno validation (bad_msg codes 32/34/35); default enforced.
|
|
43
|
+
* Interop shim for clients that don't set seqno to spec. */
|
|
44
|
+
disableSeqNoCheck?: boolean
|
|
45
|
+
/** Observability sink; defaults to a no-op logger. */
|
|
46
|
+
logger?: Logger
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Central message pipeline. Routes plaintext (handshake) vs encrypted packets,
|
|
51
|
+
* performs MTProto 2.0 decrypt/encrypt, ensures the session, and hands decoded
|
|
52
|
+
* payloads to the dispatcher. Implements {@link Responder} so the dispatcher and
|
|
53
|
+
* session manager can send encrypted replies.
|
|
54
|
+
*/
|
|
55
|
+
export class MessagePipeline implements Responder {
|
|
56
|
+
/** Set once after construction (the dispatcher needs this pipeline as its Responder). */
|
|
57
|
+
dispatcher!: Dispatcher
|
|
58
|
+
private readonly binder: PresenceBinder
|
|
59
|
+
private readonly log: Logger
|
|
60
|
+
|
|
61
|
+
constructor(private readonly deps: PipelineDeps) {
|
|
62
|
+
this.binder = deps.binder ?? new NoopPresenceBinder()
|
|
63
|
+
this.log = deps.logger ?? noopLogger
|
|
64
|
+
// Surface the insecure interop shim ONCE at startup, not per message.
|
|
65
|
+
if (deps.disableMsgKeyCheck) {
|
|
66
|
+
this.log.warn('enc.msgkey.disabled', {
|
|
67
|
+
insecure: true,
|
|
68
|
+
hint: 'inbound ciphertext integrity not verified; see docs/internals/msgkey-v1-quirk.md',
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async handlePacket(packet: Buffer, conn: Connection): Promise<void> {
|
|
74
|
+
if (packet.length < 8) {
|
|
75
|
+
conn.close()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
// Any inbound traffic resets a pending ping_delay_disconnect idle timer.
|
|
79
|
+
conn.resetDisconnect()
|
|
80
|
+
const isPlaintext = packet.readUInt32LE(0) === 0 && packet.readUInt32LE(4) === 0
|
|
81
|
+
if (isPlaintext) return this.handlePlaintext(packet, conn)
|
|
82
|
+
return this.handleEncrypted(packet, conn)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- plaintext (handshake) ---------------------------------------------
|
|
86
|
+
|
|
87
|
+
private async handlePlaintext(packet: Buffer, conn: Connection): Promise<void> {
|
|
88
|
+
// [auth_key_id=0 (8)][msg_id (8)][len (4)][body]
|
|
89
|
+
if (packet.length < 20) return conn.close()
|
|
90
|
+
const len = packet.readUInt32LE(16)
|
|
91
|
+
if (len < 4 || packet.length < 20 + len) return conn.close()
|
|
92
|
+
|
|
93
|
+
const reader = new TlReader(packet.subarray(20, 20 + len))
|
|
94
|
+
const id = reader.readUInt32()
|
|
95
|
+
if (!Handshake.isHandshakeId(id)) return
|
|
96
|
+
|
|
97
|
+
const res = await this.deps.handshake.handle(id, reader)
|
|
98
|
+
if (!res) return
|
|
99
|
+
if ('raw' in res) {
|
|
100
|
+
conn.send(res.raw)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
this.sendPlain(conn, res.reply)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private sendPlain(conn: Connection, body: TlObject): void {
|
|
107
|
+
const bodyBuf = this.deps.codec.encode(body)
|
|
108
|
+
const w = new TlWriter(bodyBuf.length + 24)
|
|
109
|
+
w.writeLong(0n) // auth_key_id
|
|
110
|
+
w.writeLong(conn.nextMessageId())
|
|
111
|
+
w.writeUInt32(bodyBuf.length)
|
|
112
|
+
w.writeRaw(bodyBuf)
|
|
113
|
+
conn.send(w.toBuffer())
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- encrypted ----------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
private async handleEncrypted(packet: Buffer, conn: Connection): Promise<void> {
|
|
119
|
+
if (packet.length < 24) return conn.close()
|
|
120
|
+
const authKeyId = toBigIntLE(packet.subarray(0, 8))
|
|
121
|
+
const msgKey = packet.subarray(8, 24)
|
|
122
|
+
const ciphertext = packet.subarray(24)
|
|
123
|
+
if (ciphertext.length % 16 !== 0 || ciphertext.length === 0) {
|
|
124
|
+
this.log.debug('enc.badlen', { authKeyId, len: ciphertext.length })
|
|
125
|
+
return conn.close()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const rec = await this.deps.storage.authKeys.getById(authKeyId)
|
|
129
|
+
this.log.debug('enc.key', { authKeyId, found: !!rec, blocked: rec?.isBlocked })
|
|
130
|
+
if (!rec || rec.isBlocked) return conn.close()
|
|
131
|
+
|
|
132
|
+
const { aesKey, aesIv } = generateMessageKey(rec.key, msgKey, false)
|
|
133
|
+
const plain = igeDecrypt(ciphertext, aesKey, aesIv)
|
|
134
|
+
|
|
135
|
+
// Inbound msg_key integrity (MTProto 2.0): the packet's msg_key must equal
|
|
136
|
+
// SHA256(authKey[88:120] ‖ plaintext)[8:24]. This authenticates the ciphertext
|
|
137
|
+
// and binds it to the auth key. Disabling the check (deps.disableMsgKeyCheck)
|
|
138
|
+
// is insecure and only intended as a temporary interop shim for non-compliant
|
|
139
|
+
// clients — see docs/internals/msgkey-v1-quirk.md.
|
|
140
|
+
if (!this.deps.disableMsgKeyCheck) {
|
|
141
|
+
if (!computeMsgKey(rec.key, plain, false).equals(msgKey)) {
|
|
142
|
+
// Ciphertext failed integrity/binding — a forged or non-2.0 client.
|
|
143
|
+
this.log.warn('enc.msgkey.reject', { authKeyId })
|
|
144
|
+
return conn.close()
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const r = new TlReader(plain)
|
|
149
|
+
const salt = r.readLong()
|
|
150
|
+
const sessionId = r.readLong()
|
|
151
|
+
const msgId = r.readLong()
|
|
152
|
+
const seqNo = r.readUInt32()
|
|
153
|
+
const len = r.readUInt32()
|
|
154
|
+
this.log.trace('enc.ok', { authKeyId, len })
|
|
155
|
+
if (len < 4 || len > plain.length) return conn.close()
|
|
156
|
+
const payload = r.read(len)
|
|
157
|
+
|
|
158
|
+
// Bind auth/session state to the connection.
|
|
159
|
+
conn.ctx.authKeyId = authKeyId
|
|
160
|
+
conn.ctx.authKey = rec.key
|
|
161
|
+
conn.ctx.sessionId = sessionId
|
|
162
|
+
if (rec.meta?.apiLayer && conn.ctx.apiLayer === this.deps.defaultLayer) {
|
|
163
|
+
conn.ctx.apiLayer = rec.meta.apiLayer
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Server-salt schedule: advertise the current salt and validate the one the
|
|
167
|
+
// client encrypted with. A wrong/expired salt earns a `bad_server_salt`
|
|
168
|
+
// carrying the current salt — the client re-sends with it — and we drop this
|
|
169
|
+
// message. See docs/internals/protocol-compliance.md.
|
|
170
|
+
const { current, valid } = await this.deps.saltService.resolve(authKeyId, salt)
|
|
171
|
+
conn.ctx.serverSalt = current
|
|
172
|
+
if (!valid) {
|
|
173
|
+
this.log.debug('salt.bad', { authKeyId })
|
|
174
|
+
this.sendEncrypted(
|
|
175
|
+
conn,
|
|
176
|
+
{
|
|
177
|
+
_: 'bad_server_salt',
|
|
178
|
+
bad_msg_id: msgId,
|
|
179
|
+
bad_msg_seqno: seqNo,
|
|
180
|
+
error_code: 48,
|
|
181
|
+
new_server_salt: current,
|
|
182
|
+
},
|
|
183
|
+
{ contentRelated: false },
|
|
184
|
+
)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Inbound msg_id / seqno validation (https://core.telegram.org/mtproto/description):
|
|
189
|
+
// a wrong/duplicate/out-of-window msg_id (or bad seqno) earns a
|
|
190
|
+
// `bad_msg_notification` and the message is dropped (not dispatched, no session
|
|
191
|
+
// touched). Runs after the salt gate so a salt re-send keeps its original
|
|
192
|
+
// msg_id. Content-relatedness is read from the payload's constructor id. See
|
|
193
|
+
// docs/internals/protocol-compliance.md.
|
|
194
|
+
const seqCheck = !this.deps.disableSeqNoCheck
|
|
195
|
+
const check = conn.tracker.accept(msgId, seqNo, {
|
|
196
|
+
...messageClass(payload),
|
|
197
|
+
checkSeqNo: seqCheck,
|
|
198
|
+
checkOrder: seqCheck,
|
|
199
|
+
})
|
|
200
|
+
if (!check.ok) {
|
|
201
|
+
this.log.debug('msg.rejected', { authKeyId, result: check })
|
|
202
|
+
replyToBadAccept(this, conn, check, msgId, seqNo)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await ensureSession(
|
|
207
|
+
this.deps.storage,
|
|
208
|
+
this,
|
|
209
|
+
conn,
|
|
210
|
+
{ sessionId, authKeyId, firstMsgId: msgId, subject: rec.subject ?? undefined },
|
|
211
|
+
this.log,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
// Register presence: by auth key (any connection — enables anonymous push)
|
|
215
|
+
// and, for already-authorized keys, by subject.
|
|
216
|
+
this.binder.bindAuthKey(conn, authKeyId.toString())
|
|
217
|
+
if (conn.ctx.subject !== undefined) this.binder.bind(conn, conn.ctx.subject)
|
|
218
|
+
|
|
219
|
+
const ctx: MessageContext = { msgId, seqNo, sessionId, authKeyId, salt }
|
|
220
|
+
await this.dispatcher.dispatchPayload(payload, ctx, conn)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- Responder ----------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
sendEncrypted(conn: Connection, body: TlObject, opts: SendOptions = {}): void {
|
|
226
|
+
const authKey = conn.ctx.authKey
|
|
227
|
+
const authKeyId = conn.ctx.authKeyId
|
|
228
|
+
if (!authKey || authKeyId === undefined) return
|
|
229
|
+
|
|
230
|
+
const bodyBuf = this.deps.codec.encode(body, conn.ctx.apiLayer)
|
|
231
|
+
const outMsgId = conn.nextMessageId(opts.isNotification)
|
|
232
|
+
// Cache the answer to a request so a later duplicate of it gets a
|
|
233
|
+
// `msg_detailed_info` (the request id is the `rpc_result.req_msg_id`).
|
|
234
|
+
if (body._ === 'rpc_result' && typeof body.req_msg_id === 'bigint') {
|
|
235
|
+
conn.tracker.recordAnswer(body.req_msg_id, outMsgId, bodyBuf.length)
|
|
236
|
+
}
|
|
237
|
+
const w = new TlWriter(bodyBuf.length + 32)
|
|
238
|
+
w.writeLong(conn.ctx.serverSalt ?? 0n)
|
|
239
|
+
w.writeLong(conn.ctx.sessionId ?? 0n)
|
|
240
|
+
w.writeLong(outMsgId)
|
|
241
|
+
w.writeUInt32(conn.nextSeqNo(opts.contentRelated ?? true))
|
|
242
|
+
w.writeUInt32(bodyBuf.length)
|
|
243
|
+
w.writeRaw(bodyBuf)
|
|
244
|
+
let plain = w.toBuffer()
|
|
245
|
+
|
|
246
|
+
// MTProto 2.0 padding: 12..1024 random bytes, total length divisible by 16.
|
|
247
|
+
const minPad = 12
|
|
248
|
+
const pad = minPad + ((16 - ((plain.length + minPad) % 16)) % 16)
|
|
249
|
+
plain = Buffer.concat([plain, randomBytes(pad)])
|
|
250
|
+
|
|
251
|
+
const msgKey = computeMsgKey(authKey, plain, true)
|
|
252
|
+
const { aesKey, aesIv } = generateMessageKey(authKey, msgKey, true)
|
|
253
|
+
const ciphertext = igeEncrypt(plain, aesKey, aesIv)
|
|
254
|
+
conn.send(Buffer.concat([toBufferLE(authKeyId, 8), msgKey, ciphertext]))
|
|
255
|
+
}
|
|
256
|
+
}
|