diff --git a/.config/vite-utils/package-json.js b/.config/vite-utils/package-json.js index 3b65a0af..57a74cfc 100644 --- a/.config/vite-utils/package-json.js +++ b/.config/vite-utils/package-json.js @@ -53,6 +53,11 @@ export function processPackageJson(packageDir) { const dependencies = packageJson[field] for (const name of Object.keys(dependencies)) { + if (name.startsWith('@fuman/')) { + delete dependencies[name] // fuman is bundled with vite for now + continue + } + const value = dependencies[name] if (value.startsWith('workspace:')) { diff --git a/.config/vite.build.ts b/.config/vite.build.ts index b2804562..ffe60001 100644 --- a/.config/vite.build.ts +++ b/.config/vite.build.ts @@ -45,6 +45,7 @@ if (typeof globalThis !== 'undefined' && !globalThis._MTCUTE_CJS_DEPRECATION_WAR ...(customConfig?.rollupPluginsPre ?? []), nodeExternals({ builtinsPrefix: 'ignore', + exclude: /^@fuman\//, }), { name: 'mtcute-finalize', diff --git a/.dockerignore b/.dockerignore index 83410924..f2dc5ce8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,3 @@ **/node_modules -**/private **/dist /e2e \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 605c767c..233fbdf8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,6 +14,12 @@ jobs: if: github.actor != 'mtcute-bot' # do not run after release steps: - uses: actions/checkout@v4 + - name: Fetch fuman + uses: actions/checkout@v4 + with: + repository: teidesu/fuman + path: private/fuman + token: ${{ secrets.BOT_PAT }} - uses: ./.github/actions/init - name: 'TypeScript' run: pnpm run lint:tsc:ci @@ -30,6 +36,12 @@ jobs: node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v4 + - name: Fetch fuman + uses: actions/checkout@v4 + with: + repository: teidesu/fuman + path: private/fuman + token: ${{ secrets.BOT_PAT }} - uses: ./.github/actions/init with: node-version: ${{ matrix.node-version }} @@ -47,6 +59,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Fetch fuman + uses: actions/checkout@v4 + with: + repository: teidesu/fuman + path: private/fuman + token: ${{ secrets.BOT_PAT }} - uses: ./.github/actions/init - uses: oven-sh/setup-bun@v1 with: @@ -60,6 +78,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Fetch fuman + uses: actions/checkout@v4 + with: + repository: teidesu/fuman + path: private/fuman + token: ${{ secrets.BOT_PAT }} - uses: ./.github/actions/init - uses: denoland/setup-deno@v1 with: @@ -77,6 +101,12 @@ jobs: browser: [chromium, firefox] steps: - uses: actions/checkout@v4 + - name: Fetch fuman + uses: actions/checkout@v4 + with: + repository: teidesu/fuman + path: private/fuman + token: ${{ secrets.BOT_PAT }} - name: 'Build Docker image' run: docker build . -f .github/Dockerfile.test-web --build-arg BROWSER=${{ matrix.browser }} -t mtcute/test-web - name: 'Run tests' @@ -96,6 +126,12 @@ jobs: actions: write steps: - uses: actions/checkout@v4 + - name: Fetch fuman + uses: actions/checkout@v4 + with: + repository: teidesu/fuman + path: private/fuman + token: ${{ secrets.BOT_PAT }} - name: Run end-to-end tests env: API_ID: ${{ secrets.TELEGRAM_API_ID }} @@ -110,22 +146,22 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REGISTRY: 'https://npm.tei.su' run: cd e2e/node && ./cli.sh ci-publish - e2e-deno: - runs-on: ubuntu-latest - needs: [lint, test-node, test-web, test-bun, test-deno] - permissions: - contents: read - actions: write - steps: - - uses: actions/checkout@v4 - - name: Run end-to-end tests under Deno - env: - API_ID: ${{ secrets.TELEGRAM_API_ID }} - API_HASH: ${{ secrets.TELEGRAM_API_HASH }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: nick-fields/retry@v2 - # thanks docker networking very cool - with: - max_attempts: 3 - timeout_minutes: 30 - command: cd e2e/deno && ./cli.sh ci + # e2e-deno: + # runs-on: ubuntu-latest + # needs: [lint, test-node, test-web, test-bun, test-deno] + # permissions: + # contents: read + # actions: write + # steps: + # - uses: actions/checkout@v4 + # - name: Run end-to-end tests under Deno + # env: + # API_ID: ${{ secrets.TELEGRAM_API_ID }} + # API_HASH: ${{ secrets.TELEGRAM_API_HASH }} + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # uses: nick-fields/retry@v2 + # # thanks docker networking very cool + # with: + # max_attempts: 3 + # timeout_minutes: 30 + # command: cd e2e/deno && ./cli.sh ci diff --git a/packages/bun/src/client.ts b/packages/bun/src/client.ts index fe030f54..3aa53c20 100644 --- a/packages/bun/src/client.ts +++ b/packages/bun/src/client.ts @@ -18,7 +18,7 @@ import { downloadAsNodeStream } from './methods/download-node-stream.js' import { BunPlatform } from './platform.js' import { SqliteStorage } from './sqlite/index.js' import { BunCryptoProvider } from './utils/crypto.js' -import { TcpTransport } from './utils/tcp.js' +// import { TcpTransport } from './utils/tcp.js' export type { TelegramClientOptions } @@ -49,7 +49,7 @@ export class BaseTelegramClient extends BaseTelegramClientBase { super({ crypto: new BunCryptoProvider(), - transport: () => new TcpTransport(), + transport: {} as any, // todo ...opts, storage: typeof opts.storage === 'string' diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 87f84c57..1b2031f3 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -2,7 +2,7 @@ export * from './client.js' export * from './platform.js' export * from './sqlite/index.js' export * from './utils/crypto.js' -export * from './utils/tcp.js' +// export * from './utils/tcp.js' export * from './worker.js' export * from '@mtcute/core' export * from '@mtcute/html-parser' diff --git a/packages/bun/src/utils/tcp.ts b/packages/bun/src/utils/tcp.ts index fbf6aa8f..b12952a2 100644 --- a/packages/bun/src/utils/tcp.ts +++ b/packages/bun/src/utils/tcp.ts @@ -1,154 +1,152 @@ -// eslint-disable-next-line unicorn/prefer-node-protocol -import EventEmitter from 'events' +// todo +// import EventEmitter from 'node:events' -import type { Socket } from 'bun' -import type { IPacketCodec, ITelegramTransport } from '@mtcute/core' -import { IntermediatePacketCodec, MtcuteError, TransportState } from '@mtcute/core' -import type { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js' +// import { IntermediatePacketCodec, IPacketCodec, ITelegramConnection, MtcuteError } from '@mtcute/core' +// import { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js' -/** - * Base for TCP transports. - * Subclasses must provide packet codec in `_packetCodec` property - */ -export abstract class BaseTcpTransport extends EventEmitter implements ITelegramTransport { - protected _currentDc: BasicDcOption | null = null - protected _state: TransportState = TransportState.Idle - protected _socket: Socket | null = null +// /** +// * Base for TCP transports. +// * Subclasses must provide packet codec in `_packetCodec` property +// */ +// export abstract class BaseTcpTransport extends EventEmitter implements ITelegramConnection { +// protected _currentDc: BasicDcOption | null = null +// protected _state: TransportState = TransportState.Idle +// protected _socket: Socket | null = null - abstract _packetCodec: IPacketCodec - protected _crypto!: ICryptoProvider - protected log!: Logger +// abstract _packetCodec: IPacketCodec +// protected _crypto!: ICryptoProvider +// protected log!: Logger - packetCodecInitialized = false +// packetCodecInitialized = false - private _updateLogPrefix() { - if (this._currentDc) { - this.log.prefix = `[TCP:${this._currentDc.ipAddress}:${this._currentDc.port}] ` - } else { - this.log.prefix = '[TCP:disconnected] ' - } - } +// private _updateLogPrefix() { +// if (this._currentDc) { +// this.log.prefix = `[TCP:${this._currentDc.ipAddress}:${this._currentDc.port}] ` +// } else { +// this.log.prefix = '[TCP:disconnected] ' +// } +// } - setup(crypto: ICryptoProvider, log: Logger): void { - this._crypto = crypto - this.log = log.create('tcp') - this._updateLogPrefix() - } +// setup(crypto: ICryptoProvider, log: Logger): void { +// this._crypto = crypto +// this.log = log.create('tcp') +// this._updateLogPrefix() +// } - state(): TransportState { - return this._state - } +// state(): TransportState { +// return this._state +// } - currentDc(): BasicDcOption | null { - return this._currentDc - } +// currentDc(): BasicDcOption | null { +// return this._currentDc +// } - // eslint-disable-next-line unused-imports/no-unused-vars - connect(dc: BasicDcOption, testMode: boolean): void { - if (this._state !== TransportState.Idle) { - throw new MtcuteError('Transport is not IDLE') - } +// // eslint-disable-next-line unused-imports/no-unused-vars +// connect(dc: BasicDcOption, testMode: boolean): void { +// if (this._state !== TransportState.Idle) { +// throw new MtcuteError('Transport is not IDLE') +// } - if (!this.packetCodecInitialized) { - this._packetCodec.setup?.(this._crypto, this.log) - this._packetCodec.on('error', err => this.emit('error', err)) - this._packetCodec.on('packet', buf => this.emit('message', buf)) - this.packetCodecInitialized = true - } +// if (!this.packetCodecInitialized) { +// this._packetCodec.setup?.(this._crypto, this.log) +// this._packetCodec.on('error', err => this.emit('error', err)) +// this._packetCodec.on('packet', buf => this.emit('message', buf)) +// this.packetCodecInitialized = true +// } - this._state = TransportState.Connecting - this._currentDc = dc - this._updateLogPrefix() +// this._state = TransportState.Connecting +// this._currentDc = dc +// this._updateLogPrefix() - this.log.debug('connecting to %j', dc) +// this.log.debug('connecting to %j', dc) - Bun.connect({ - hostname: dc.ipAddress, - port: dc.port, - socket: { - open: this.handleConnect.bind(this), - error: this.handleError.bind(this), - data: (socket, data) => this._packetCodec.feed(data), - close: this.close.bind(this), - drain: this.handleDrained.bind(this), - }, - }).catch((err) => { - this.handleError(null, err as Error) - this.close() - }) - } +// Bun.connect({ +// hostname: dc.ipAddress, +// port: dc.port, +// socket: { +// open: this.handleConnect.bind(this), +// error: this.handleError.bind(this), +// data: (socket, data) => this._packetCodec.feed(data), +// close: this.close.bind(this), +// drain: this.handleDrained.bind(this), +// }, +// }).catch((err) => { +// this.handleError(null, err as Error) +// this.close() +// }) +// } - close(): void { - if (this._state === TransportState.Idle) return - this.log.info('connection closed') +// close(): void { +// if (this._state === TransportState.Idle) return +// this.log.info('connection closed') - this._state = TransportState.Idle - this._socket?.end() - this._socket = null - this._currentDc = null - this._packetCodec.reset() - this._sendOnceDrained = [] - this.emit('close') - } +// this._state = TransportState.Idle +// this._socket?.end() +// this._socket = null +// this._currentDc = null +// this._packetCodec.reset() +// this._sendOnceDrained = [] +// this.emit('close') +// } - handleError(socket: unknown, error: Error): void { - this.log.error('error: %s', error.stack) +// handleError(socket: unknown, error: Error): void { +// this.log.error('error: %s', error.stack) - if (this.listenerCount('error') > 0) { - this.emit('error', error) - } - } +// if (this.listenerCount('error') > 0) { +// this.emit('error', error) +// } +// } - handleConnect(socket: Socket): void { - this._socket = socket - this.log.info('connected') +// handleConnect(socket: Socket): void { +// this._socket = socket +// this.log.info('connected') - Promise.resolve(this._packetCodec.tag()) - .then((initialMessage) => { - if (initialMessage.length) { - this._socket!.write(initialMessage) - this._state = TransportState.Ready - this.emit('ready') - } else { - this._state = TransportState.Ready - this.emit('ready') - } - }) - .catch((err) => { - if (this.listenerCount('error') > 0) { - this.emit('error', err) - } - }) - } +// Promise.resolve(this._packetCodec.tag()) +// .then((initialMessage) => { +// if (initialMessage.length) { +// this._socket!.write(initialMessage) +// this._state = TransportState.Ready +// this.emit('ready') +// } else { +// this._state = TransportState.Ready +// this.emit('ready') +// } +// }) +// .catch((err) => { +// if (this.listenerCount('error') > 0) { +// this.emit('error', err) +// } +// }) +// } - async send(bytes: Uint8Array): Promise { - const framed = await this._packetCodec.encode(bytes) +// async send(bytes: Uint8Array): Promise { +// const framed = await this._packetCodec.encode(bytes) - if (this._state !== TransportState.Ready) { - throw new MtcuteError('Transport is not READY') - } +// if (this._state !== TransportState.Ready) { +// throw new MtcuteError('Transport is not READY') +// } - const written = this._socket!.write(framed) +// const written = this._socket!.write(framed) - if (written < framed.length) { - this._sendOnceDrained.push(framed.subarray(written)) - } - } +// if (written < framed.length) { +// this._sendOnceDrained.push(framed.subarray(written)) +// } +// } - private _sendOnceDrained: Uint8Array[] = [] - private handleDrained(): void { - while (this._sendOnceDrained.length) { - const data = this._sendOnceDrained.shift()! - const written = this._socket!.write(data) +// private _sendOnceDrained: Uint8Array[] = [] +// private handleDrained(): void { +// while (this._sendOnceDrained.length) { +// const data = this._sendOnceDrained.shift()! +// const written = this._socket!.write(data) - if (written < data.length) { - this._sendOnceDrained.unshift(data.subarray(written)) - break - } - } - } -} +// if (written < data.length) { +// this._sendOnceDrained.unshift(data.subarray(written)) +// break +// } +// } +// } +// } -export class TcpTransport extends BaseTcpTransport { - _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec() -} +// export class TcpTransport extends BaseTcpTransport { +// _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec() +// } diff --git a/packages/core/package.json b/packages/core/package.json index 13bc0d85..3b730197 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,36 +1,39 @@ { - "name": "@mtcute/core", - "type": "module", - "version": "0.16.13", - "private": true, - "description": "Type-safe library for MTProto (Telegram API)", - "author": "alina sireneva ", - "license": "MIT", - "sideEffects": false, - "exports": { - ".": "./src/index.ts", - "./utils.js": "./src/utils/index.ts", - "./client.js": "./src/highlevel/client.ts", - "./worker.js": "./src/highlevel/worker/index.ts", - "./methods.js": "./src/highlevel/methods.ts", - "./platform.js": "./src/platform.ts" - }, - "scripts": { - "build": "pnpm run -w build-package core", - "gen-client": "node ./scripts/generate-client.cjs", - "gen-updates": "node ./scripts/generate-updates.cjs" - }, - "dependencies": { - "@mtcute/file-id": "workspace:^", - "@mtcute/tl": "workspace:^", - "@mtcute/tl-runtime": "workspace:^", - "@types/events": "3.0.0", - "events": "3.2.0", - "long": "5.2.3" - }, - "devDependencies": { - "@mtcute/test": "workspace:^", - "@types/ws": "8.5.4", - "ws": "8.13.0" - } + "name": "@mtcute/core", + "type": "module", + "version": "0.16.13", + "private": true, + "description": "Type-safe library for MTProto (Telegram API)", + "author": "alina sireneva ", + "license": "MIT", + "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./utils.js": "./src/utils/index.ts", + "./client.js": "./src/highlevel/client.ts", + "./worker.js": "./src/highlevel/worker/index.ts", + "./methods.js": "./src/highlevel/methods.ts", + "./platform.js": "./src/platform.ts" + }, + "scripts": { + "build": "pnpm run -w build-package core", + "gen-client": "node ./scripts/generate-client.cjs", + "gen-updates": "node ./scripts/generate-updates.cjs" + }, + "dependencies": { + "@fuman/io": "workspace:^", + "@fuman/net": "workspace:^", + "@fuman/utils": "workspace:^", + "@mtcute/file-id": "workspace:^", + "@mtcute/tl": "workspace:^", + "@mtcute/tl-runtime": "workspace:^", + "@types/events": "3.0.0", + "events": "3.2.0", + "long": "5.2.3" + }, + "devDependencies": { + "@mtcute/test": "workspace:^", + "@types/ws": "8.5.4", + "ws": "8.13.0" + } } diff --git a/packages/core/src/network/client.ts b/packages/core/src/network/client.ts index 792dffc6..19246b30 100644 --- a/packages/core/src/network/client.ts +++ b/packages/core/src/network/client.ts @@ -6,6 +6,7 @@ import { tl } from '@mtcute/tl' import { __tlReaderMap as defaultReaderMap } from '@mtcute/tl/binary/reader.js' import { __tlWriterMap as defaultWriterMap } from '@mtcute/tl/binary/writer.js' import type { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' +import type { ReconnectionStrategy } from '@fuman/net' import type { IMtStorageProvider } from '../storage/provider.js' import type { StorageManagerExtraOptions } from '../storage/storage.js' @@ -29,9 +30,7 @@ import { import { ConfigManager } from './config-manager.js' import type { NetworkManagerExtraParams, RpcCallOptions } from './network-manager.js' import { NetworkManager } from './network-manager.js' -import type { PersistentConnectionParams } from './persistent-connection.js' -import type { ReconnectionStrategy } from './reconnection.js' -import type { TransportFactory } from './transports/index.js' +import type { TelegramTransport } from './transports' /** Options for {@link MtClient} */ export interface MtClientOptions { @@ -96,18 +95,18 @@ export interface MtClientOptions { initConnectionOptions?: Partial> /** - * Transport factory to use in the client. + * Transport to use in the client. * * @default platform-specific transport: WebSocket on the web, TCP in node */ - transport: TransportFactory + transport: TelegramTransport /** * Reconnection strategy. * * @default simple reconnection strategy: first 0ms, then up to 5s (increasing by 1s) */ - reconnectionStrategy?: ReconnectionStrategy + reconnectionStrategy?: ReconnectionStrategy /** * If true, all API calls will be wrapped with `tl.invokeWithoutUpdates`, diff --git a/packages/core/src/network/index.ts b/packages/core/src/network/index.ts index abd57885..dfb492c4 100644 --- a/packages/core/src/network/index.ts +++ b/packages/core/src/network/index.ts @@ -7,6 +7,6 @@ export type { RpcCallMiddlewareContext, RpcCallOptions, } from './network-manager.js' -export * from './reconnection.js' +// export * from './reconnection.js' export * from './session-connection.js' export * from './transports/index.js' diff --git a/packages/core/src/network/multi-session-connection.ts b/packages/core/src/network/multi-session-connection.ts index 66650098..1232f764 100644 --- a/packages/core/src/network/multi-session-connection.ts +++ b/packages/core/src/network/multi-session-connection.ts @@ -9,7 +9,7 @@ import { createControllablePromise } from '../utils/index.js' import { MtprotoSession } from './mtproto-session.js' import type { SessionConnectionParams } from './session-connection.js' import { SessionConnection } from './session-connection.js' -import type { TransportFactory } from './transports/index.js' +import type { TelegramTransport } from './transports' export class MultiSessionConnection extends EventEmitter { private _log: Logger @@ -323,7 +323,7 @@ export class MultiSessionConnection extends EventEmitter { } } - changeTransport(factory: TransportFactory): void { + changeTransport(factory: TelegramTransport): void { this._connections.forEach(conn => conn.changeTransport(factory)) } diff --git a/packages/core/src/network/network-manager.ts b/packages/core/src/network/network-manager.ts index 1aeec755..61211a89 100644 --- a/packages/core/src/network/network-manager.ts +++ b/packages/core/src/network/network-manager.ts @@ -1,6 +1,7 @@ import type { mtp, tl } from '@mtcute/tl' import type { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' import type Long from 'long' +import { type ReconnectionStrategy, defaultReconnectionStrategy } from '@fuman/net' import { getPlatform } from '../platform.js' import type { StorageManager } from '../storage/storage.js' @@ -14,12 +15,9 @@ import { assertTypeIs, isTlRpcError } from '../utils/type-assertions.js' import type { ConfigManager } from './config-manager.js' import { basic as defaultMiddlewares } from './middlewares/default.js' import { MultiSessionConnection } from './multi-session-connection.js' -import type { PersistentConnectionParams } from './persistent-connection.js' -import type { ReconnectionStrategy } from './reconnection.js' -import { defaultReconnectionStrategy } from './reconnection.js' import { ServerSaltManager } from './server-salt.js' import type { SessionConnection, SessionConnectionParams } from './session-connection.js' -import type { TransportFactory } from './transports/index.js' +import type { TelegramTransport } from './transports/abstract.js' export type ConnectionKind = 'main' | 'upload' | 'download' | 'downloadSmall' @@ -35,8 +33,8 @@ export interface NetworkManagerParams { enableErrorReporting: boolean apiId: number initConnectionOptions?: Partial> - transport: TransportFactory - reconnectionStrategy?: ReconnectionStrategy + transport: TelegramTransport + reconnectionStrategy?: ReconnectionStrategy disableUpdates?: boolean testMode: boolean layer: number @@ -241,7 +239,7 @@ export class DcConnectionManager { const baseConnectionParams = (): SessionConnectionParams => ({ crypto: this.manager.params.crypto, initConnection: this.manager._initConnectionParams, - transportFactory: this.manager._transportFactory, + transport: this.manager._transport, dc: this._dcs.media, testMode: this.manager.params.testMode, reconnectionStrategy: this.manager._reconnectionStrategy, @@ -474,8 +472,8 @@ export class NetworkManager { readonly _storage: StorageManager readonly _initConnectionParams: tl.RawInitConnectionRequest - readonly _transportFactory: TransportFactory - readonly _reconnectionStrategy: ReconnectionStrategy + readonly _transport: TelegramTransport + readonly _reconnectionStrategy: ReconnectionStrategy readonly _connectionCount: ConnectionCountDelegate protected readonly _dcConnections: Map = new Map() @@ -503,7 +501,7 @@ export class NetworkManager { query: null as any, } - this._transportFactory = params.transport + this._transport = params.transport this._reconnectionStrategy = params.reconnectionStrategy ?? defaultReconnectionStrategy this._connectionCount = params.connectionCount ?? defaultConnectionCountDelegate this._updateHandler = params.onUpdate @@ -858,12 +856,12 @@ export class NetworkManager { return res } - changeTransport(factory: TransportFactory): void { + changeTransport(transport: TelegramTransport): void { for (const dc of this._dcConnections.values()) { - dc.main.changeTransport(factory) - dc.upload.changeTransport(factory) - dc.download.changeTransport(factory) - dc.downloadSmall.changeTransport(factory) + dc.main.changeTransport(transport) + dc.upload.changeTransport(transport) + dc.download.changeTransport(transport) + dc.downloadSmall.changeTransport(transport) } } diff --git a/packages/core/src/network/persistent-connection.test.ts b/packages/core/src/network/persistent-connection.test.ts index 8f60f23e..f5a53ff4 100644 --- a/packages/core/src/network/persistent-connection.test.ts +++ b/packages/core/src/network/persistent-connection.test.ts @@ -1,223 +1,224 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { StubTelegramTransport, createStub, defaultTestCryptoProvider } from '@mtcute/test' +// todo: move to fuman +// import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +// import { StubTelegramTransport, createStub, defaultTestCryptoProvider } from '@mtcute/test' -import { LogManager, timers } from '../utils/index.js' +// import { LogManager, timers } from '../utils/index.js' -import type { PersistentConnectionParams } from './persistent-connection.js' -import { PersistentConnection } from './persistent-connection.js' -import { defaultReconnectionStrategy } from './reconnection.js' +// import type { PersistentConnectionParams } from './persistent-connection.js' +// import { PersistentConnection } from './persistent-connection.js' +// import { defaultReconnectionStrategy } from './reconnection.js' -class FakePersistentConnection extends PersistentConnection { - constructor(params: PersistentConnectionParams) { - const log = new LogManager() - log.level = 0 - super(params, log) - } +// class FakePersistentConnection extends PersistentConnection { +// constructor(params: PersistentConnectionParams) { +// const log = new LogManager() +// log.level = 0 +// super(params, log) +// } - onConnected() { - this.onConnectionUsable() - } +// onConnected() { +// this.onConnectionUsable() +// } - onError() {} - onMessage() {} -} +// onError() {} +// onMessage() {} +// } -describe('PersistentConnection', () => { - beforeEach(() => void vi.useFakeTimers()) - afterEach(() => void vi.useRealTimers()) +// describe('PersistentConnection', () => { +// beforeEach(() => void vi.useFakeTimers()) +// afterEach(() => void vi.useRealTimers()) - const create = async (params?: Partial) => { - return new FakePersistentConnection({ - crypto: await defaultTestCryptoProvider(), - transportFactory: () => new StubTelegramTransport({}), - dc: createStub('dcOption'), - testMode: false, - reconnectionStrategy: defaultReconnectionStrategy, - ...params, - }) - } +// const create = async (params?: Partial) => { +// return new FakePersistentConnection({ +// crypto: await defaultTestCryptoProvider(), +// transportFactory: () => new StubTelegramTransport({}), +// dc: createStub('dcOption'), +// testMode: false, +// reconnectionStrategy: defaultReconnectionStrategy, +// ...params, +// }) +// } - it('should set up listeners on transport', async () => { - const transportFactory = vi.fn().mockImplementation(() => { - const transport = new StubTelegramTransport({}) +// it('should set up listeners on transport', async () => { +// const transportFactory = vi.fn().mockImplementation(() => { +// const transport = new StubTelegramTransport({}) - vi.spyOn(transport, 'on') +// vi.spyOn(transport, 'on') - return transport - }) - await create({ transportFactory }) +// return transport +// }) +// await create({ transportFactory }) - const transport = transportFactory.mock.results[0].value as StubTelegramTransport +// const transport = transportFactory.mock.results[0].value as StubTelegramTransport - expect(transport.on).toHaveBeenCalledWith('ready', expect.any(Function)) - expect(transport.on).toHaveBeenCalledWith('message', expect.any(Function)) - expect(transport.on).toHaveBeenCalledWith('error', expect.any(Function)) - expect(transport.on).toHaveBeenCalledWith('close', expect.any(Function)) - }) +// expect(transport.on).toHaveBeenCalledWith('ready', expect.any(Function)) +// expect(transport.on).toHaveBeenCalledWith('message', expect.any(Function)) +// expect(transport.on).toHaveBeenCalledWith('error', expect.any(Function)) +// expect(transport.on).toHaveBeenCalledWith('close', expect.any(Function)) +// }) - it('should properly reset old transport', async () => { - const transportFactory = vi.fn().mockImplementation(() => { - const transport = new StubTelegramTransport({}) +// it('should properly reset old transport', async () => { +// const transportFactory = vi.fn().mockImplementation(() => { +// const transport = new StubTelegramTransport({}) - vi.spyOn(transport, 'close') +// vi.spyOn(transport, 'close') - return transport - }) - const pc = await create({ transportFactory }) +// return transport +// }) +// const pc = await create({ transportFactory }) - const transport = transportFactory.mock.results[0].value as StubTelegramTransport +// const transport = transportFactory.mock.results[0].value as StubTelegramTransport - pc.changeTransport(transportFactory) +// pc.changeTransport(transportFactory) - expect(transport.close).toHaveBeenCalledOnce() - }) +// expect(transport.close).toHaveBeenCalledOnce() +// }) - it('should buffer unsent packages', async () => { - const transportFactory = vi.fn().mockImplementation(() => { - const transport = new StubTelegramTransport({}) +// it('should buffer unsent packages', async () => { +// const transportFactory = vi.fn().mockImplementation(() => { +// const transport = new StubTelegramTransport({}) - const transportConnect = transport.connect - vi.spyOn(transport, 'connect').mockImplementation((dc, test) => { - timers.setTimeout(() => { - transportConnect.call(transport, dc, test) - }, 100) - }) - vi.spyOn(transport, 'send') +// const transportConnect = transport.connect +// vi.spyOn(transport, 'connect').mockImplementation((dc, test) => { +// timers.setTimeout(() => { +// transportConnect.call(transport, dc, test) +// }, 100) +// }) +// vi.spyOn(transport, 'send') - return transport - }) - const pc = await create({ transportFactory }) +// return transport +// }) +// const pc = await create({ transportFactory }) - const transport = transportFactory.mock.results[0].value as StubTelegramTransport +// const transport = transportFactory.mock.results[0].value as StubTelegramTransport - const data1 = new Uint8Array([1, 2, 3]) - const data2 = new Uint8Array([4, 5, 6]) +// const data1 = new Uint8Array([1, 2, 3]) +// const data2 = new Uint8Array([4, 5, 6]) - await pc.send(data1) - await pc.send(data2) +// await pc.send(data1) +// await pc.send(data2) - expect(transport.send).toHaveBeenCalledTimes(0) +// expect(transport.send).toHaveBeenCalledTimes(0) - await vi.advanceTimersByTimeAsync(150) +// await vi.advanceTimersByTimeAsync(150) - expect(transport.send).toHaveBeenCalledTimes(2) - expect(transport.send).toHaveBeenCalledWith(data1) - expect(transport.send).toHaveBeenCalledWith(data2) - }) +// expect(transport.send).toHaveBeenCalledTimes(2) +// expect(transport.send).toHaveBeenCalledWith(data1) +// expect(transport.send).toHaveBeenCalledWith(data2) +// }) - it('should reconnect on close', async () => { - const reconnectionStrategy = vi.fn().mockImplementation(() => 1000) - const transportFactory = vi.fn().mockImplementation(() => new StubTelegramTransport({})) +// it('should reconnect on close', async () => { +// const reconnectionStrategy = vi.fn().mockImplementation(() => 1000) +// const transportFactory = vi.fn().mockImplementation(() => new StubTelegramTransport({})) - const pc = await create({ - reconnectionStrategy, - transportFactory, - }) +// const pc = await create({ +// reconnectionStrategy, +// transportFactory, +// }) - const transport = transportFactory.mock.results[0].value as StubTelegramTransport +// const transport = transportFactory.mock.results[0].value as StubTelegramTransport - pc.connect() +// pc.connect() - await vi.waitFor(() => expect(pc.isConnected).toBe(true)) +// await vi.waitFor(() => expect(pc.isConnected).toBe(true)) - transport.close() +// transport.close() - expect(reconnectionStrategy).toHaveBeenCalledOnce() - expect(pc.isConnected).toBe(false) +// expect(reconnectionStrategy).toHaveBeenCalledOnce() +// expect(pc.isConnected).toBe(false) - await vi.advanceTimersByTimeAsync(1000) +// await vi.advanceTimersByTimeAsync(1000) - expect(pc.isConnected).toBe(true) - }) +// expect(pc.isConnected).toBe(true) +// }) - describe('inactivity timeout', () => { - it('should disconnect on inactivity (passed in constructor)', async () => { - const pc = await create({ - inactivityTimeout: 1000, - }) +// describe('inactivity timeout', () => { +// it('should disconnect on inactivity (passed in constructor)', async () => { +// const pc = await create({ +// inactivityTimeout: 1000, +// }) - pc.connect() +// pc.connect() - await vi.waitFor(() => expect(pc.isConnected).toBe(true)) +// await vi.waitFor(() => expect(pc.isConnected).toBe(true)) - vi.advanceTimersByTime(1000) +// vi.advanceTimersByTime(1000) - await vi.waitFor(() => expect(pc.isConnected).toBe(false)) - }) +// await vi.waitFor(() => expect(pc.isConnected).toBe(false)) +// }) - it('should disconnect on inactivity (set up with setInactivityTimeout)', async () => { - const pc = await create() +// it('should disconnect on inactivity (set up with setInactivityTimeout)', async () => { +// const pc = await create() - pc.connect() - pc.setInactivityTimeout(1000) +// pc.connect() +// pc.setInactivityTimeout(1000) - await vi.waitFor(() => expect(pc.isConnected).toBe(true)) +// await vi.waitFor(() => expect(pc.isConnected).toBe(true)) - vi.advanceTimersByTime(1000) +// vi.advanceTimersByTime(1000) - await vi.waitFor(() => expect(pc.isConnected).toBe(false)) - }) +// await vi.waitFor(() => expect(pc.isConnected).toBe(false)) +// }) - it('should not disconnect on inactivity if disabled', async () => { - const pc = await create({ - inactivityTimeout: 1000, - }) +// it('should not disconnect on inactivity if disabled', async () => { +// const pc = await create({ +// inactivityTimeout: 1000, +// }) - pc.connect() - pc.setInactivityTimeout(undefined) +// pc.connect() +// pc.setInactivityTimeout(undefined) - await vi.waitFor(() => expect(pc.isConnected).toBe(true)) +// await vi.waitFor(() => expect(pc.isConnected).toBe(true)) - vi.advanceTimersByTime(1000) +// vi.advanceTimersByTime(1000) - await vi.waitFor(() => expect(pc.isConnected).toBe(true)) - }) +// await vi.waitFor(() => expect(pc.isConnected).toBe(true)) +// }) - it('should reconnect after inactivity before sending', async () => { - const transportFactory = vi.fn().mockImplementation(() => { - const transport = new StubTelegramTransport({}) +// it('should reconnect after inactivity before sending', async () => { +// const transportFactory = vi.fn().mockImplementation(() => { +// const transport = new StubTelegramTransport({}) - vi.spyOn(transport, 'connect') - vi.spyOn(transport, 'send') +// vi.spyOn(transport, 'connect') +// vi.spyOn(transport, 'send') - return transport - }) +// return transport +// }) - const pc = await create({ - inactivityTimeout: 1000, - transportFactory, - }) - const transport = transportFactory.mock.results[0].value as StubTelegramTransport +// const pc = await create({ +// inactivityTimeout: 1000, +// transportFactory, +// }) +// const transport = transportFactory.mock.results[0].value as StubTelegramTransport - pc.connect() +// pc.connect() - vi.advanceTimersByTime(1000) +// vi.advanceTimersByTime(1000) - await vi.waitFor(() => expect(pc.isConnected).toBe(false)) +// await vi.waitFor(() => expect(pc.isConnected).toBe(false)) - vi.mocked(transport.connect).mockClear() +// vi.mocked(transport.connect).mockClear() - await pc.send(new Uint8Array([1, 2, 3])) +// await pc.send(new Uint8Array([1, 2, 3])) - expect(transport.connect).toHaveBeenCalledOnce() - expect(transport.send).toHaveBeenCalledOnce() - }) +// expect(transport.connect).toHaveBeenCalledOnce() +// expect(transport.send).toHaveBeenCalledOnce() +// }) - it('should propagate errors', async () => { - const transportFactory = vi.fn().mockImplementation(() => new StubTelegramTransport({})) +// it('should propagate errors', async () => { +// const transportFactory = vi.fn().mockImplementation(() => new StubTelegramTransport({})) - const pc = await create({ transportFactory }) - const transport = transportFactory.mock.results[0].value as StubTelegramTransport +// const pc = await create({ transportFactory }) +// const transport = transportFactory.mock.results[0].value as StubTelegramTransport - pc.connect() +// pc.connect() - await vi.waitFor(() => expect(pc.isConnected).toBe(true)) +// await vi.waitFor(() => expect(pc.isConnected).toBe(true)) - const onErrorSpy = vi.spyOn(pc, 'onError') +// const onErrorSpy = vi.spyOn(pc, 'onError') - transport.emit('error', new Error('test error')) +// transport.emit('error', new Error('test error')) - expect(onErrorSpy).toHaveBeenCalledOnce() - }) - }) -}) +// expect(onErrorSpy).toHaveBeenCalledOnce() +// }) +// }) +// }) diff --git a/packages/core/src/network/persistent-connection.ts b/packages/core/src/network/persistent-connection.ts index 0ee18d36..44d2ea8d 100644 --- a/packages/core/src/network/persistent-connection.ts +++ b/packages/core/src/network/persistent-connection.ts @@ -1,20 +1,21 @@ // eslint-disable-next-line unicorn/prefer-node-protocol import EventEmitter from 'events' -import { MtcuteError } from '../types/index.js' +import type { ReconnectionStrategy } from '@fuman/net' +import { PersistentConnection as FumanPersistentConnection } from '@fuman/net' +import { FramedReader, FramedWriter } from '@fuman/io' + import type { BasicDcOption, ICryptoProvider, Logger } from '../utils/index.js' import { timers } from '../utils/index.js' -import type { ReconnectionStrategy } from './reconnection.js' -import type { ITelegramTransport, TransportFactory } from './transports/index.js' -import { TransportState } from './transports/index.js' +import type { IPacketCodec, ITelegramConnection, TelegramTransport } from './transports/abstract.js' export interface PersistentConnectionParams { crypto: ICryptoProvider - transportFactory: TransportFactory + transport: TelegramTransport dc: BasicDcOption testMode: boolean - reconnectionStrategy: ReconnectionStrategy + reconnectionStrategy: ReconnectionStrategy inactivityTimeout?: number } @@ -29,9 +30,11 @@ export abstract class PersistentConnection extends EventEmitter { private _uid = nextConnectionUid++ readonly params: PersistentConnectionParams - protected _transport!: ITelegramTransport + // protected _transport!: ITelegramConnection private _sendOnceConnected: Uint8Array[] = [] + private _codec: IPacketCodec + private _fuman: FumanPersistentConnection // reconnection private _lastError: Error | null = null @@ -49,6 +52,7 @@ export abstract class PersistentConnection extends EventEmitter { _usable = false protected abstract onConnected(): void + protected abstract onClosed(): void protected abstract onError(err: Error): void @@ -60,174 +64,233 @@ export abstract class PersistentConnection extends EventEmitter { ) { super() this.params = params - this.changeTransport(params.transportFactory) - this.log.prefix = `[UID ${this._uid}] ` + this._codec = this.params.transport.packetCodec() + this._codec.setup?.(this.params.crypto, this.log) + this._onInactivityTimeout = this._onInactivityTimeout.bind(this) + this._fuman = new FumanPersistentConnection({ + connect: dc => params.transport.connect(dc, params.testMode), + onOpen: this._onOpen.bind(this), + onClose: this._onClose.bind(this), + onError: this._onError.bind(this), + onWait: wait => this.emit('wait', wait), + }) } get isConnected(): boolean { - return this._transport.state() !== TransportState.Idle + return this._fuman.isConnected } - changeTransport(factory: TransportFactory): void { - if (this._transport) { - Promise.resolve(this._transport.close()).catch((err) => { - this.log.warn('error closing previous transport: %e', err) - }) - } + private _writer?: FramedWriter - this._transport = factory() - this._transport.setup?.(this.params.crypto, this.log) + private async _onOpen(conn: ITelegramConnection) { + await conn.write(await this._codec.tag()) - this._transport.on('ready', this.onTransportReady.bind(this)) - this._transport.on('message', this.onMessage.bind(this)) - this._transport.on('error', this.onTransportError.bind(this)) - this._transport.on('close', this.onTransportClose.bind(this)) - } + const reader = new FramedReader(conn, this._codec) + this._writer = new FramedWriter(conn, this._codec) - onTransportReady(): void { - // transport ready does not mean actual mtproto is ready - if (this._sendOnceConnected.length) { - const sendNext = () => { - if (!this._sendOnceConnected.length) { - this.onConnected() + while (this._sendOnceConnected.length) { + const data = this._sendOnceConnected.shift()! - return - } - - const data = this._sendOnceConnected.shift()! - this._transport - .send(data) - .then(sendNext) - .catch((err) => { - this.log.error('error sending queued data: %e', err) - this._sendOnceConnected.unshift(data) - }) + try { + await this._writer.write(data) + } catch (e) { + this._sendOnceConnected.unshift(data) + throw e } - - sendNext() - - return } - this.onConnected() - } - - protected onConnectionUsable(): void { - const isReconnection = this._consequentFails > 0 - - // reset reconnection related state - this._lastError = null - this._consequentFails = 0 - this._previousWait = null - this._usable = true - this.emit('usable', isReconnection) this._rescheduleInactivity() + this.emit('usable') // is this needed? + this.onConnected() + + while (true) { + const msg = await reader.read() + + if (msg) { + this.onMessage(msg) + } + } } - onTransportError(err: Error): void { + private async _onClose() { + this._writer = undefined + this._codec.reset() + this.onClosed() + } + + private async _onError(err: Error) { this._lastError = err this.onError(err) - // transport is expected to emit `close` after `error` } - onTransportClose(): void { - // transport closed because of inactivity - // obviously we dont want to reconnect then - if (this._inactive || this._disconnectedManually) return + async changeTransport(transport: TelegramTransport): Promise { + await this._fuman.close() - if (this._shouldReconnectImmediately) { - this._shouldReconnectImmediately = false - this.connect() + this._codec = transport.packetCodec() + this._codec.setup?.(this.params.crypto, this.log) - return - } + await this._fuman.changeTransport(() => transport.connect(this.params.dc, this.params.testMode)) + this._fuman.connect(this.params.dc) + // if (this._transport) { + // Promise.resolve(this._transport.close()).catch((err) => { + // this.log.warn('error closing previous transport: %e', err) + // }) + // } - this._consequentFails += 1 + // this._transport = conn() + // this._transport.setup?.(this.params.crypto, this.log) - const wait = this.params.reconnectionStrategy( - this.params, - this._lastError, - this._consequentFails, - this._previousWait, - ) - - if (wait === false) { - this.destroy().catch((err) => { - this.log.warn('error destroying connection: %e', err) - }) - - return - } - - this.emit('wait', wait) - - this._previousWait = wait - - if (this._reconnectionTimeout != null) { - timers.clearTimeout(this._reconnectionTimeout) - } - this._reconnectionTimeout = timers.setTimeout(() => { - if (this._destroyed) return - this._reconnectionTimeout = null - this.connect() - }, wait) + // this._transport.on('ready', this.onTransportReady.bind(this)) + // this._transport.on('message', this.onMessage.bind(this)) + // this._transport.on('error', this.onTransportError.bind(this)) + // this._transport.on('close', this.onTransportClose.bind(this)) } + // onTransportReady(): void { + // // transport ready does not mean actual mtproto is ready + // if (this._sendOnceConnected.length) { + // const sendNext = () => { + // if (!this._sendOnceConnected.length) { + // this.onConnected() + + // return + // } + + // const data = this._sendOnceConnected.shift()! + // this._transport + // .send(data) + // .then(sendNext) + // .catch((err) => { + // this.log.error('error sending queued data: %e', err) + // this._sendOnceConnected.unshift(data) + // }) + // } + + // sendNext() + + // return + // } + + // this.onConnected() + // } + + // protected onConnectionUsable(): void { + // const isReconnection = this._consequentFails > 0 + + // // reset reconnection related state + // this._lastError = null + // this._consequentFails = 0 + // this._previousWait = null + // this._usable = true + // this.emit('usable', isReconnection) + // this._rescheduleInactivity() + // } + + // onTransportError(err: Error): void { + + // // transport is expected to emit `close` after `error` + // } + + // onTransportClose(): void { + // // transport closed because of inactivity + // // obviously we dont want to reconnect then + // if (this._inactive || this._disconnectedManually) return + + // if (this._shouldReconnectImmediately) { + // this._shouldReconnectImmediately = false + // this.connect() + + // return + // } + + // this._consequentFails += 1 + + // const wait = this.params.reconnectionStrategy( + // this.params, + // this._lastError, + // this._consequentFails, + // this._previousWait, + // ) + + // if (wait === false) { + // this.destroy().catch((err) => { + // this.log.warn('error destroying connection: %e', err) + // }) + + // return + // } + + // this._previousWait = wait + + // if (this._reconnectionTimeout != null) { + // timers.clearTimeout(this._reconnectionTimeout) + // } + // this._reconnectionTimeout = timers.setTimeout(() => { + // if (this._destroyed) return + // this._reconnectionTimeout = null + // this.connect() + // }, wait) + // } + connect(): void { - if (this.isConnected) { - throw new MtcuteError('Connection is already opened!') - } - if (this._destroyed) { - throw new MtcuteError('Connection is already destroyed!') - } - - if (this._reconnectionTimeout != null) { - timers.clearTimeout(this._reconnectionTimeout) - this._reconnectionTimeout = null - } + this._fuman.connect(this.params.dc) this._inactive = false - this._disconnectedManually = false - this._transport.connect(this.params.dc, this.params.testMode) + // if (this.isConnected) { + // throw new MtcuteError('Connection is already opened!') + // } + // if (this._destroyed) { + // throw new MtcuteError('Connection is already destroyed!') + // } + + // if (this._reconnectionTimeout != null) { + // clearTimeout(this._reconnectionTimeout) + // this._reconnectionTimeout = null + // } + + // this._inactive = false + // this._disconnectedManually = false + // this._transport.connect(this.params.dc, this.params.testMode) } reconnect(): void { - if (this._inactive) return + // if (this._inactive) return - // if we are already connected - if (this.isConnected) { - this._shouldReconnectImmediately = true - Promise.resolve(this._transport.close()).catch((err) => { - this.log.error('error closing transport: %e', err) - }) + // // if we are already connected + // if (this.isConnected) { + // this._shouldReconnectImmediately = true + // Promise.resolve(this._transport.close()).catch((err) => { + // this.log.error('error closing transport: %e', err) + // }) - return - } + // return + // } - // if reconnection timeout is pending, it will be cancelled in connect() - this.connect() + // // if reconnection timeout is pending, it will be cancelled in connect() + // this.connect() + this._fuman.reconnect(true) } async disconnectManual(): Promise { - this._disconnectedManually = true - await this._transport.close() + // this._disconnectedManually = true + // await this._transport.close() + await this._fuman.close() } async destroy(): Promise { - this._disconnectedManually = true - if (this._reconnectionTimeout != null) { - timers.clearTimeout(this._reconnectionTimeout) - } - if (this._inactivityTimeout != null) { - timers.clearTimeout(this._inactivityTimeout) - } + // if (this._reconnectionTimeout != null) { + // clearTimeout(this._reconnectionTimeout) + // } + // if (this._inactivityTimeout != null) { + // clearTimeout(this._inactivityTimeout) + // } - await this._transport.close() - this._transport.removeAllListeners() - this._destroyed = true + await this._fuman.close() + // this._transport.removeAllListeners() + // this._destroyed = true } protected _rescheduleInactivity(): void { @@ -240,7 +303,7 @@ export abstract class PersistentConnection extends EventEmitter { this.log.info('disconnected because of inactivity for %d', this.params.inactivityTimeout) this._inactive = true this._inactivityTimeout = null - Promise.resolve(this._transport.close()).catch((err) => { + Promise.resolve(this._fuman.close()).catch((err) => { this.log.warn('error closing transport: %e', err) }) } @@ -262,8 +325,9 @@ export abstract class PersistentConnection extends EventEmitter { this.connect() } - if (this._transport.state() === TransportState.Ready) { - await this._transport.send(data) + if (this._writer) { + this._rescheduleInactivity() + await this._writer.write(data) } else { this._sendOnceConnected.push(data) } diff --git a/packages/core/src/network/reconnection.ts b/packages/core/src/network/reconnection.ts deleted file mode 100644 index c289a133..00000000 --- a/packages/core/src/network/reconnection.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Declares a strategy to handle reconnection. - * When a number is returned, that number of MS will be waited before trying to reconnect. - * When `false` is returned, connection is not reconnected - */ -export type ReconnectionStrategy = ( - params: T, - lastError: Error | null, - consequentFails: number, - previousWait: number | null, -) => number | false - -/** - * default reconnection strategy: first - immediate reconnection, - * then 1s with linear increase up to 5s (with 1s step) - */ -export const defaultReconnectionStrategy: ReconnectionStrategy = ( - params, - lastError, - consequentFails, - previousWait, -) => { - if (previousWait === null) return 0 - if (previousWait === 0) return 1000 - if (previousWait >= 5000) return 5000 - - return Math.min(5000, previousWait + 1000) -} diff --git a/packages/core/src/network/session-connection.ts b/packages/core/src/network/session-connection.ts index 21c76776..f48b195c 100644 --- a/packages/core/src/network/session-connection.ts +++ b/packages/core/src/network/session-connection.ts @@ -131,9 +131,7 @@ export class SessionConnection extends PersistentConnection { this._resetSession() } - onTransportClose(): void { - super.onTransportClose() - + onClosed(): void { Object.values(this._pendingWaitForUnencrypted).forEach(([prom, timeout]) => { prom.reject(new MtcuteError('Connection closed')) timers.clearTimeout(timeout) @@ -262,8 +260,6 @@ export class SessionConnection extends PersistentConnection { } protected onConnectionUsable(): void { - super.onConnectionUsable() - if (this.params.withUpdates) { // we must send some user-related rpc to the server to make sure that // it will send us updates @@ -1363,13 +1359,13 @@ export class SessionConnection extends PersistentConnection { // either acked or returns rpc_result this.log.debug('wrapping %s with initConnection, layer: %d', method, this.params.layer) - const proxy = this._transport.getMtproxyInfo?.() + // const proxy = this._transport.getMtproxyInfo?.() obj = { _: 'invokeWithLayer', layer: this.params.layer, query: { ...this.params.initConnection, - proxy, + // proxy, query: obj, }, } diff --git a/packages/core/src/network/transports/abstract.ts b/packages/core/src/network/transports/abstract.ts index a7a9d526..6485fe47 100644 --- a/packages/core/src/network/transports/abstract.ts +++ b/packages/core/src/network/transports/abstract.ts @@ -1,58 +1,14 @@ -// eslint-disable-next-line unicorn/prefer-node-protocol -import type EventEmitter from 'events' - import type { tl } from '@mtcute/tl' +import type { IFrameDecoder, IFrameEncoder } from '@fuman/io' +import type { IConnection } from '@fuman/net' import type { MaybePromise } from '../../types/index.js' import type { BasicDcOption, ICryptoProvider, Logger } from '../../utils/index.js' -/** Current state of the transport */ -export enum TransportState { - /** - * Transport has no active connections nor trying to connect to anything - * Can be a result of network failure, close() call, or connect() call was - * never called on this instance of Transport. - * - * This state is usually immediately handled by more higher-level components, - * thus rarely seen - */ - Idle = 'idle', - /** - * Transport has no active connections, but is trying to connect to something - */ - Connecting = 'connecting', - /** - * Transport has an active connection - */ - Ready = 'ready', -} - /** * Interface implementing a transport to interact with Telegram servers. - * - * Events: - * - `ready` event is emitted once connection has been established: `() => void` - * - `close` event is emitted once connection has been closed: `() => void` - * - `error` event is event is emitted when there was some error - * (either mtproto related or network related): `(error: Error) => void` - * - `message` event is emitted when a mtproto message is received: `(message: Buffer) => void` */ -export interface ITelegramTransport extends EventEmitter { - /** returns current state */ - state(): TransportState - /** returns current DC. should return null if state == IDLE */ - currentDc(): BasicDcOption | null - - /** - * Start trying to connect to a specified DC. - * Will throw an error if state != IDLE - */ - connect(dc: BasicDcOption, testMode: boolean): void - /** call to close existing connection to some DC */ - close(): MaybePromise - /** send a message */ - send(data: Uint8Array): Promise - +export interface ITelegramConnection extends IConnection { /** * Provides crypto and logging for the transport. * Not done in constructor to simplify factory. @@ -64,36 +20,20 @@ export interface ITelegramTransport extends EventEmitter { getMtproxyInfo?(): tl.RawInputClientProxy } -/** Transport factory function */ -export type TransportFactory = () => ITelegramTransport +export interface TelegramTransport { + connect: (dc: BasicDcOption, testMode: boolean) => Promise + packetCodec: () => IPacketCodec +} /** * Interface declaring handling of received packets. * When receiving a packet, its content is sent to feed(), * and codec is supposed to emit `packet` or `error` event when packet is parsed. */ -export interface IPacketCodec { +export interface IPacketCodec extends IFrameDecoder, IFrameEncoder { /** Initial tag of the codec. Will be sent immediately once connected. */ tag(): MaybePromise - /** Encodes and frames a single packet */ - encode(packet: Uint8Array): MaybePromise - - /** Feed packet to the codec. Once packet is processed, codec is supposed to emit `packet` or `error` */ - feed(data: Uint8Array): void - /** Reset codec state (for example, reset buffer) */ - reset(): void - - /** Remove all listeners from a codec, used to safely dispose it */ - removeAllListeners(): void - - /** - * Emitted when a packet containing a - * (Transport error)[https://core.telegram.org/mtproto/mtproto-transports#transport-errors] is encountered. - */ - on(event: 'error', handler: (error: Error) => void): void - on(event: 'packet', handler: (packet: Uint8Array) => void): void - /** * For codecs that use crypto functions and/or logging. * This method is called before any other. diff --git a/packages/core/src/network/transports/index.ts b/packages/core/src/network/transports/index.ts index 8251b88a..97f369da 100644 --- a/packages/core/src/network/transports/index.ts +++ b/packages/core/src/network/transports/index.ts @@ -1,5 +1,3 @@ export * from './abstract.js' export * from './intermediate.js' export * from './obfuscated.js' -export * from './streamed.js' -export * from './wrapped.js' diff --git a/packages/core/src/network/transports/intermediate.test.ts b/packages/core/src/network/transports/intermediate.test.ts index ab98a5d3..d5b79814 100644 --- a/packages/core/src/network/transports/intermediate.test.ts +++ b/packages/core/src/network/transports/intermediate.test.ts @@ -1,108 +1,109 @@ -import { describe, expect, it } from 'vitest' -import { defaultTestCryptoProvider, useFakeMathRandom } from '@mtcute/test' +// todo: fix test +// import { describe, expect, it } from 'vitest' +// import { defaultTestCryptoProvider, useFakeMathRandom } from '@mtcute/test' -import { IntermediatePacketCodec, PaddedIntermediatePacketCodec, TransportError } from '../../index.js' -import { getPlatform } from '../../platform.js' +// import { IntermediatePacketCodec, PaddedIntermediatePacketCodec, TransportError } from '../../index.js' +// import { getPlatform } from '../../platform.js' -const p = getPlatform() +// const p = getPlatform() -describe('IntermediatePacketCodec', () => { - it('should return correct tag', () => { - expect(p.hexEncode(new IntermediatePacketCodec().tag())).eq('eeeeeeee') - }) +// describe('IntermediatePacketCodec', () => { +// it('should return correct tag', () => { +// expect(p.hexEncode(new IntermediatePacketCodec().tag())).eq('eeeeeeee') +// }) - it('should correctly parse immediate framing', () => - new Promise((done) => { - const codec = new IntermediatePacketCodec() - codec.on('packet', (data: Uint8Array) => { - expect([...data]).eql([5, 1, 2, 3, 4]) - done() - }) - codec.feed(p.hexDecode('050000000501020304')) - })) +// it('should correctly parse immediate framing', () => +// new Promise((done) => { +// const codec = new IntermediatePacketCodec() +// codec.on('packet', (data: Uint8Array) => { +// expect([...data]).eql([5, 1, 2, 3, 4]) +// done() +// }) +// codec.feed(p.hexDecode('050000000501020304')) +// })) - it('should correctly parse incomplete framing', () => - new Promise((done) => { - const codec = new IntermediatePacketCodec() - codec.on('packet', (data: Uint8Array) => { - expect([...data]).eql([5, 1, 2, 3, 4]) - done() - }) - codec.feed(p.hexDecode('050000000501')) - codec.feed(p.hexDecode('020304')) - })) +// it('should correctly parse incomplete framing', () => +// new Promise((done) => { +// const codec = new IntermediatePacketCodec() +// codec.on('packet', (data: Uint8Array) => { +// expect([...data]).eql([5, 1, 2, 3, 4]) +// done() +// }) +// codec.feed(p.hexDecode('050000000501')) +// codec.feed(p.hexDecode('020304')) +// })) - it('should correctly parse multiple streamed packets', () => - new Promise((done) => { - const codec = new IntermediatePacketCodec() +// it('should correctly parse multiple streamed packets', () => +// new Promise((done) => { +// const codec = new IntermediatePacketCodec() - let number = 0 +// let number = 0 - codec.on('packet', (data: Uint8Array) => { - if (number === 0) { - expect([...data]).eql([5, 1, 2, 3, 4]) - number = 1 - } else { - expect([...data]).eql([3, 1, 2, 3, 1]) - done() - } - }) - codec.feed(p.hexDecode('050000000501')) - codec.feed(p.hexDecode('020304050000')) - codec.feed(p.hexDecode('000301020301')) - })) +// codec.on('packet', (data: Uint8Array) => { +// if (number === 0) { +// expect([...data]).eql([5, 1, 2, 3, 4]) +// number = 1 +// } else { +// expect([...data]).eql([3, 1, 2, 3, 1]) +// done() +// } +// }) +// codec.feed(p.hexDecode('050000000501')) +// codec.feed(p.hexDecode('020304050000')) +// codec.feed(p.hexDecode('000301020301')) +// })) - it('should correctly parse transport errors', () => - new Promise((done) => { - const codec = new IntermediatePacketCodec() +// it('should correctly parse transport errors', () => +// new Promise((done) => { +// const codec = new IntermediatePacketCodec() - codec.on('error', (err: TransportError) => { - expect(err).to.have.instanceOf(TransportError) - expect(err.code).eq(404) - done() - }) +// codec.on('error', (err: TransportError) => { +// expect(err).to.have.instanceOf(TransportError) +// expect(err.code).eq(404) +// done() +// }) - codec.feed(p.hexDecode('040000006cfeffff')) - })) +// codec.feed(p.hexDecode('040000006cfeffff')) +// })) - it('should reset when called reset()', () => - new Promise((done) => { - const codec = new IntermediatePacketCodec() +// it('should reset when called reset()', () => +// new Promise((done) => { +// const codec = new IntermediatePacketCodec() - codec.on('packet', (data: Uint8Array) => { - expect([...data]).eql([1, 2, 3, 4, 5]) - done() - }) +// codec.on('packet', (data: Uint8Array) => { +// expect([...data]).eql([1, 2, 3, 4, 5]) +// done() +// }) - codec.feed(p.hexDecode('ff0000001234567812345678')) - codec.reset() - codec.feed(p.hexDecode('050000000102030405')) - })) +// codec.feed(p.hexDecode('ff0000001234567812345678')) +// codec.reset() +// codec.feed(p.hexDecode('050000000102030405')) +// })) - it('should correctly frame packets', () => { - const data = p.hexDecode('6cfeffff') +// it('should correctly frame packets', () => { +// const data = p.hexDecode('6cfeffff') - expect(p.hexEncode(new IntermediatePacketCodec().encode(data))).toEqual('040000006cfeffff') - }) -}) +// expect(p.hexEncode(new IntermediatePacketCodec().encode(data))).toEqual('040000006cfeffff') +// }) +// }) -describe('PaddedIntermediatePacketCodec', () => { - useFakeMathRandom() +// describe('PaddedIntermediatePacketCodec', () => { +// useFakeMathRandom() - const create = async () => { - const codec = new PaddedIntermediatePacketCodec() - codec.setup!(await defaultTestCryptoProvider()) +// const create = async () => { +// const codec = new PaddedIntermediatePacketCodec() +// codec.setup!(await defaultTestCryptoProvider()) - return codec - } +// return codec +// } - it('should return correct tag', async () => { - expect(p.hexEncode((await create()).tag())).eq('dddddddd') - }) +// it('should return correct tag', async () => { +// expect(p.hexEncode((await create()).tag())).eq('dddddddd') +// }) - it('should correctly frame packets', async () => { - const data = p.hexDecode('6cfeffff') +// it('should correctly frame packets', async () => { +// const data = p.hexDecode('6cfeffff') - expect(p.hexEncode((await create()).encode(data))).toEqual('0a0000006cfeffff29afd26df40f') - }) -}) +// expect(p.hexEncode((await create()).encode(data))).toEqual('0a0000006cfeffff29afd26df40f') +// }) +// }) diff --git a/packages/core/src/network/transports/intermediate.ts b/packages/core/src/network/transports/intermediate.ts index 9c75af2c..efc83aee 100644 --- a/packages/core/src/network/transports/intermediate.ts +++ b/packages/core/src/network/transports/intermediate.ts @@ -1,9 +1,11 @@ +import type { Bytes, ISyncWritable } from '@fuman/io' +import { read, write } from '@fuman/io' + import type { ICryptoProvider } from '../../utils/index.js' import { dataViewFromBuffer, getRandomInt } from '../../utils/index.js' import type { IPacketCodec } from './abstract.js' import { TransportError } from './abstract.js' -import { StreamedCodec } from './streamed.js' const TAG = new Uint8Array([0xEE, 0xEE, 0xEE, 0xEE]) const PADDED_TAG = new Uint8Array([0xDD, 0xDD, 0xDD, 0xDD]) @@ -12,51 +14,46 @@ const PADDED_TAG = new Uint8Array([0xDD, 0xDD, 0xDD, 0xDD]) * Intermediate packet codec. * See https://core.telegram.org/mtproto/mtproto-transports#intermediate */ -export class IntermediatePacketCodec extends StreamedCodec implements IPacketCodec { +export class IntermediatePacketCodec implements IPacketCodec { tag(): Uint8Array { return TAG } - encode(packet: Uint8Array): Uint8Array { - const ret = new Uint8Array(packet.length + 4) - const dv = dataViewFromBuffer(ret) - dv.setUint32(0, packet.length, true) - ret.set(packet, 4) + decode(reader: Bytes, eof: boolean): Uint8Array | null { + if (eof) return null - return ret - } + if (reader.available < 8) return null - protected _packetAvailable(): boolean { - return this._stream.length >= 8 - } + const length = read.uint32le(reader) - protected _handlePacket(): boolean { - const dv = dataViewFromBuffer(this._stream) - const payloadLength = dv.getUint32(0, true) - - if (payloadLength <= this._stream.length - 4) { - if (payloadLength === 4) { - const code = dv.getInt32(4, true) * -1 - this.emit('error', new TransportError(code)) - } else { - const payload = this._stream.subarray(4, payloadLength + 4) - this.emit('packet', payload) - } - - this._stream = this._stream.subarray(payloadLength + 4) - - return true + if (length === 4) { + // error + const code = read.uint32le(reader) + throw new TransportError(code) } - return false + if (reader.available < length) { + reader.unread(4) + + return null + } + + return read.exactly(reader, length) } + + encode(frame: Uint8Array, into: ISyncWritable): void { + write.uint32le(into, frame.length) + write.bytes(into, frame) + } + + reset(): void {} } /** * Padded intermediate packet codec. * See https://core.telegram.org/mtproto/mtproto-transports#padded-intermediate */ -export class PaddedIntermediatePacketCodec extends IntermediatePacketCodec { +export class PaddedIntermediatePacketCodec extends IntermediatePacketCodec implements IPacketCodec { tag(): Uint8Array { return PADDED_TAG } @@ -66,16 +63,15 @@ export class PaddedIntermediatePacketCodec extends IntermediatePacketCodec { this._crypto = crypto } - encode(packet: Uint8Array): Uint8Array { + encode(frame: Uint8Array, into: ISyncWritable): void { // padding size, 0-15 const padSize = getRandomInt(16) - const ret = new Uint8Array(packet.length + 4 + padSize) + const ret = into.writeSync(frame.length + 4 + padSize) const dv = dataViewFromBuffer(ret) - dv.setUint32(0, packet.length + padSize, true) - ret.set(packet, 4) - this._crypto.randomFill(ret.subarray(4 + packet.length)) - - return ret + dv.setUint32(0, frame.length + padSize, true) + ret.set(frame, 4) + this._crypto.randomFill(ret.subarray(4 + frame.length)) + into.disposeWriteSync() } } diff --git a/packages/core/src/network/transports/obfuscated.test.ts b/packages/core/src/network/transports/obfuscated.test.ts index 4dc8d978..16611610 100644 --- a/packages/core/src/network/transports/obfuscated.test.ts +++ b/packages/core/src/network/transports/obfuscated.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test' +import { Bytes } from '@fuman/io' import { getPlatform } from '../../platform.js' import { LogManager } from '../../utils/index.js' @@ -7,6 +8,7 @@ import { LogManager } from '../../utils/index.js' import { IntermediatePacketCodec } from './intermediate.js' import type { MtProxyInfo } from './obfuscated.js' import { ObfuscatedPacketCodec } from './obfuscated.js' +import { TransportError } from './abstract' const p = getPlatform() @@ -162,8 +164,13 @@ describe('ObfuscatedPacketCodec', () => { await codec.tag() - expect(p.hexEncode(await codec.encode(data))).toEqual(msg1) - expect(p.hexEncode(await codec.encode(data))).toEqual(msg2) + const buf = Bytes.alloc() + await codec.encode(data, buf) + expect(p.hexEncode(buf.result())).toEqual(msg1) + + buf.reset() + await codec.encode(data, buf) + expect(p.hexEncode(buf.result())).toEqual(msg2) }) it('should correctly decrypt the underlying codec', async () => { @@ -174,16 +181,8 @@ describe('ObfuscatedPacketCodec', () => { await codec.tag() - const log: string[] = [] - - codec.on('error', (e: Error) => { - log.push(e.toString()) - }) - - codec.feed(p.hexDecode(msg1)) - codec.feed(p.hexDecode(msg2)) - - await vi.waitFor(() => expect(log).toEqual(['Error: Transport error: 404', 'Error: Transport error: 404'])) + expect(codec.decode(Bytes.from(p.hexDecode(msg1)), false)).rejects.toThrow(TransportError) + expect(codec.decode(Bytes.from(p.hexDecode(msg2)), false)).rejects.toThrow(TransportError) }) it('should correctly reset', async () => { diff --git a/packages/core/src/network/transports/obfuscated.ts b/packages/core/src/network/transports/obfuscated.ts index 993fbdcf..d9b9d427 100644 --- a/packages/core/src/network/transports/obfuscated.ts +++ b/packages/core/src/network/transports/obfuscated.ts @@ -1,8 +1,10 @@ +import type { ISyncWritable } from '@fuman/io' +import { Bytes, write } from '@fuman/io' + import { bufferToReversed, concatBuffers, dataViewFromBuffer } from '../../utils/buffer-utils.js' -import type { IAesCtr } from '../../utils/index.js' +import type { IAesCtr, ICryptoProvider, Logger } from '../../utils/index.js' import type { IPacketCodec } from './abstract.js' -import { WrappedCodec } from './wrapped.js' export interface MtProxyInfo { dcId: number @@ -11,14 +13,22 @@ export interface MtProxyInfo { media: boolean } -export class ObfuscatedPacketCodec extends WrappedCodec implements IPacketCodec { +export class ObfuscatedPacketCodec implements IPacketCodec { private _encryptor?: IAesCtr private _decryptor?: IAesCtr + private _crypto!: ICryptoProvider + private _inner: IPacketCodec private _proxy?: MtProxyInfo + setup(crypto: ICryptoProvider, log: Logger): void { + this._crypto = crypto + this._inner.setup?.(crypto, log) + } + constructor(inner: IPacketCodec, proxy?: MtProxyInfo) { - super(inner) + // super(inner) + this._inner = inner this._proxy = proxy } @@ -87,14 +97,17 @@ export class ObfuscatedPacketCodec extends WrappedCodec implements IPacketCodec return random } - async encode(packet: Uint8Array): Promise { - return this._encryptor!.process(await this._inner.encode(packet)) + async encode(packet: Uint8Array, into: ISyncWritable): Promise { + const temp = Bytes.alloc(packet.length) + await this._inner.encode(packet, into) + write.bytes(into, this._encryptor!.process(temp.result())) } - feed(data: Uint8Array): void { - const dec = this._decryptor!.process(data) + async decode(reader: Bytes, eof: boolean): Promise { + const inner = await this._inner.decode(reader, eof) + if (!inner) return null - this._inner.feed(dec) + return this._decryptor!.process(inner) } reset(): void { diff --git a/packages/core/src/network/transports/streamed.ts b/packages/core/src/network/transports/streamed.ts deleted file mode 100644 index 6b552458..00000000 --- a/packages/core/src/network/transports/streamed.ts +++ /dev/null @@ -1,39 +0,0 @@ -// eslint-disable-next-line unicorn/prefer-node-protocol -import EventEmitter from 'events' - -import { concatBuffers } from '../../utils/index.js' - -/** - * Base for streamed codecs. - * - * Streamed means that MTProto packet can be divided into - * multiple transport packets. - */ -export abstract class StreamedCodec extends EventEmitter { - protected _stream: Uint8Array = new Uint8Array(0) - - /** - * Should return whether a full packet is available - * in `_stream` buffer - */ - protected abstract _packetAvailable(): boolean - - /** - * Handle a single (!) packet from `_stream` and emit `packet` or `error`. - * - * Should return true if there are more packets available, false otherwise - */ - protected abstract _handlePacket(): boolean - - feed(data: Uint8Array): void { - this._stream = concatBuffers([this._stream, data]) - - while (this._packetAvailable()) { - if (!this._handlePacket()) break - } - } - - reset(): void { - this._stream = new Uint8Array(0) - } -} diff --git a/packages/core/src/network/transports/wrapped.ts b/packages/core/src/network/transports/wrapped.ts deleted file mode 100644 index 4310ef61..00000000 --- a/packages/core/src/network/transports/wrapped.ts +++ /dev/null @@ -1,30 +0,0 @@ -// eslint-disable-next-line unicorn/prefer-node-protocol -import EventEmitter from 'events' - -import type { ICryptoProvider, Logger } from '../../utils/index.js' - -import type { IPacketCodec } from './abstract.js' - -export abstract class WrappedCodec extends EventEmitter { - protected _crypto!: ICryptoProvider - protected _inner: IPacketCodec - - constructor(inner: IPacketCodec) { - super() - this._inner = inner - this._inner.on('error', err => this.emit('error', err)) - this._inner.on('packet', buf => this.emit('packet', buf)) - } - - removeAllListeners(): this { - super.removeAllListeners() - this._inner.removeAllListeners() - - return this - } - - setup(crypto: ICryptoProvider, log: Logger): void { - this._crypto = crypto - this._inner.setup?.(crypto, log) - } -} diff --git a/packages/deno/src/client.ts b/packages/deno/src/client.ts index 3649bd0b..edd02cd5 100644 --- a/packages/deno/src/client.ts +++ b/packages/deno/src/client.ts @@ -17,7 +17,6 @@ import { downloadToFile } from './methods/download-file.js' import { DenoPlatform } from './platform.js' import { SqliteStorage } from './sqlite/index.js' import { DenoCryptoProvider } from './utils/crypto.js' -import { TcpTransport } from './utils/tcp.js' export type { TelegramClientOptions } @@ -48,7 +47,7 @@ export class BaseTelegramClient extends BaseTelegramClientBase { super({ crypto: new DenoCryptoProvider(), - transport: () => new TcpTransport(), + transport: {} as any, // todo ...opts, storage: typeof opts.storage === 'string' diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 87f84c57..1b2031f3 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -2,7 +2,7 @@ export * from './client.js' export * from './platform.js' export * from './sqlite/index.js' export * from './utils/crypto.js' -export * from './utils/tcp.js' +// export * from './utils/tcp.js' export * from './worker.js' export * from '@mtcute/core' export * from '@mtcute/html-parser' diff --git a/packages/deno/src/utils/tcp.ts b/packages/deno/src/utils/tcp.ts index 88727b13..a91c8b43 100644 --- a/packages/deno/src/utils/tcp.ts +++ b/packages/deno/src/utils/tcp.ts @@ -1,146 +1,146 @@ -import EventEmitter from 'node:events' +// import EventEmitter from 'node:events' -import type { IPacketCodec, ITelegramTransport } from '@mtcute/core' -import { IntermediatePacketCodec, MtcuteError, TransportState } from '@mtcute/core' -import type { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js' -import { writeAll } from '@std/io/write-all' +// import { IntermediatePacketCodec, IPacketCodec, ITelegramTransport, MtcuteError, TransportState } from '@mtcute/core' +// import { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js' -/** - * Base for TCP transports. - * Subclasses must provide packet codec in `_packetCodec` property - */ -export abstract class BaseTcpTransport extends EventEmitter implements ITelegramTransport { - protected _currentDc: BasicDcOption | null = null - protected _state: TransportState = TransportState.Idle - protected _socket: Deno.TcpConn | null = null +// import { writeAll } from '@std/io/write-all' - abstract _packetCodec: IPacketCodec - protected _crypto!: ICryptoProvider - protected log!: Logger +// /** +// * Base for TCP transports. +// * Subclasses must provide packet codec in `_packetCodec` property +// */ +// export abstract class BaseTcpTransport extends EventEmitter implements ITelegramConnection { +// protected _currentDc: BasicDcOption | null = null +// protected _state: TransportState = TransportState.Idle +// protected _socket: Deno.TcpConn | null = null - packetCodecInitialized = false +// abstract _packetCodec: IPacketCodec +// protected _crypto!: ICryptoProvider +// protected log!: Logger - private _updateLogPrefix() { - if (this._currentDc) { - this.log.prefix = `[TCP:${this._currentDc.ipAddress}:${this._currentDc.port}] ` - } else { - this.log.prefix = '[TCP:disconnected] ' - } - } +// packetCodecInitialized = false - setup(crypto: ICryptoProvider, log: Logger): void { - this._crypto = crypto - this.log = log.create('tcp') - this._updateLogPrefix() - } +// private _updateLogPrefix() { +// if (this._currentDc) { +// this.log.prefix = `[TCP:${this._currentDc.ipAddress}:${this._currentDc.port}] ` +// } else { +// this.log.prefix = '[TCP:disconnected] ' +// } +// } - state(): TransportState { - return this._state - } +// setup(crypto: ICryptoProvider, log: Logger): void { +// this._crypto = crypto +// this.log = log.create('tcp') +// this._updateLogPrefix() +// } - currentDc(): BasicDcOption | null { - return this._currentDc - } +// state(): TransportState { +// return this._state +// } - // eslint-disable-next-line unused-imports/no-unused-vars - connect(dc: BasicDcOption, testMode: boolean): void { - if (this._state !== TransportState.Idle) { - throw new MtcuteError('Transport is not IDLE') - } +// currentDc(): BasicDcOption | null { +// return this._currentDc +// } - if (!this.packetCodecInitialized) { - this._packetCodec.setup?.(this._crypto, this.log) - this._packetCodec.on('error', err => this.emit('error', err)) - this._packetCodec.on('packet', buf => this.emit('message', buf)) - this.packetCodecInitialized = true - } +// // eslint-disable-next-line unused-imports/no-unused-vars +// connect(dc: BasicDcOption, testMode: boolean): void { +// if (this._state !== TransportState.Idle) { +// throw new MtcuteError('Transport is not IDLE') +// } - this._state = TransportState.Connecting - this._currentDc = dc - this._updateLogPrefix() +// if (!this.packetCodecInitialized) { +// this._packetCodec.setup?.(this._crypto, this.log) +// this._packetCodec.on('error', err => this.emit('error', err)) +// this._packetCodec.on('packet', buf => this.emit('message', buf)) +// this.packetCodecInitialized = true +// } - this.log.debug('connecting to %j', dc) +// this._state = TransportState.Connecting +// this._currentDc = dc +// this._updateLogPrefix() - Deno.connect({ - hostname: dc.ipAddress, - port: dc.port, - transport: 'tcp', - }) - .then(this.handleConnect.bind(this)) - .catch((err) => { - this.handleError(err) - this.close() - }) - } +// this.log.debug('connecting to %j', dc) - close(): void { - if (this._state === TransportState.Idle) return - this.log.info('connection closed') +// Deno.connect({ +// hostname: dc.ipAddress, +// port: dc.port, +// transport: 'tcp', +// }) +// .then(this.handleConnect.bind(this)) +// .catch((err) => { +// this.handleError(err) +// this.close() +// }) +// } - this._state = TransportState.Idle +// close(): void { +// if (this._state === TransportState.Idle) return +// this.log.info('connection closed') - try { - this._socket?.close() - } catch (e) { - if (!(e instanceof Deno.errors.BadResource)) { - this.handleError(e) - } - } +// this._state = TransportState.Idle - this._socket = null - this._currentDc = null - this._packetCodec.reset() - this.emit('close') - } +// try { +// this._socket?.close() +// } catch (e) { +// if (!(e instanceof Deno.errors.BadResource)) { +// this.handleError(e) +// } +// } - handleError(error: unknown): void { - this.log.error('error: %s', error) +// this._socket = null +// this._currentDc = null +// this._packetCodec.reset() +// this.emit('close') +// } - if (this.listenerCount('error') > 0) { - this.emit('error', error) - } - } +// handleError(error: unknown): void { +// this.log.error('error: %s', error) - async handleConnect(socket: Deno.TcpConn): Promise { - this._socket = socket - this.log.info('connected') +// if (this.listenerCount('error') > 0) { +// this.emit('error', error) +// } +// } - try { - const packet = await this._packetCodec.tag() +// async handleConnect(socket: Deno.TcpConn): Promise { +// this._socket = socket +// this.log.info('connected') - if (packet.length) { - await writeAll(this._socket, packet) - } +// try { +// const packet = await this._packetCodec.tag() - this._state = TransportState.Ready - this.emit('ready') +// if (packet.length) { +// await writeAll(this._socket, packet) +// } - const reader = this._socket.readable.getReader() +// this._state = TransportState.Ready +// this.emit('ready') - while (true) { - const { done, value } = await reader.read() - if (done) break +// const reader = this._socket.readable.getReader() - this._packetCodec.feed(value) - } - } catch (e) { - this.handleError(e) - } +// while (true) { +// const { done, value } = await reader.read() +// if (done) break - this.close() - } +// this._packetCodec.feed(value) +// } +// } catch (e) { +// this.handleError(e) +// } - async send(bytes: Uint8Array): Promise { - const framed = await this._packetCodec.encode(bytes) +// this.close() +// } - if (this._state !== TransportState.Ready) { - throw new MtcuteError('Transport is not READY') - } +// async send(bytes: Uint8Array): Promise { +// const framed = await this._packetCodec.encode(bytes) - await writeAll(this._socket!, framed) - } -} +// if (this._state !== TransportState.Ready) { +// throw new MtcuteError('Transport is not READY') +// } -export class TcpTransport extends BaseTcpTransport { - _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec() -} +// await writeAll(this._socket!, framed) +// } +// } + +// export class TcpTransport extends BaseTcpTransport { +// _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec() +// } diff --git a/packages/http-proxy/index.ts b/packages/http-proxy/index.ts index 30bd4755..a97f01ad 100644 --- a/packages/http-proxy/index.ts +++ b/packages/http-proxy/index.ts @@ -1,164 +1,165 @@ -import { connect as connectTcp } from 'node:net' -import type { SecureContextOptions } from 'node:tls' -import { connect as connectTls } from 'node:tls' +// todo: move to fuman +// import { connect as connectTcp } from 'node:net' +// import type { SecureContextOptions } from 'node:tls' +// import { connect as connectTls } from 'node:tls' -import type { tl } from '@mtcute/node' -import { BaseTcpTransport, IntermediatePacketCodec, MtcuteError, NodePlatform, TransportState } from '@mtcute/node' +// import type { tl } from '@mtcute/node' +// import { BaseTcpTransport, IntermediatePacketCodec, MtcuteError, NodePlatform, TransportState } from '@mtcute/node' -/** - * An error has occurred while connecting to an HTTP(s) proxy - */ -export class HttpProxyConnectionError extends Error { - readonly proxy: HttpProxySettings +// /** +// * An error has occurred while connecting to an HTTP(s) proxy +// */ +// export class HttpProxyConnectionError extends Error { +// readonly proxy: HttpProxySettings - constructor(proxy: HttpProxySettings, message: string) { - super(`Error while connecting to ${proxy.host}:${proxy.port}: ${message}`) - this.proxy = proxy - } -} +// constructor(proxy: HttpProxySettings, message: string) { +// super(`Error while connecting to ${proxy.host}:${proxy.port}: ${message}`) +// this.proxy = proxy +// } +// } -/** - * HTTP(s) proxy settings - */ -export interface HttpProxySettings { - /** - * Host or IP of the proxy (e.g. `proxy.example.com`, `1.2.3.4`) - */ - host: string +// /** +// * HTTP(s) proxy settings +// */ +// export interface HttpProxySettings { +// /** +// * Host or IP of the proxy (e.g. `proxy.example.com`, `1.2.3.4`) +// */ +// host: string - /** - * Port of the proxy (e.g. `8888`) - */ - port: number +// /** +// * Port of the proxy (e.g. `8888`) +// */ +// port: number - /** - * Proxy authorization username, if needed - */ - user?: string +// /** +// * Proxy authorization username, if needed +// */ +// user?: string - /** - * Proxy authorization password, if needed - */ - password?: string +// /** +// * Proxy authorization password, if needed +// */ +// password?: string - /** - * Proxy connection headers, if needed - */ - headers?: Record +// /** +// * Proxy connection headers, if needed +// */ +// headers?: Record - /** - * Whether this is a HTTPS proxy (i.e. the client - * should connect to the proxy server via TLS) - */ - tls?: boolean +// /** +// * Whether this is a HTTPS proxy (i.e. the client +// * should connect to the proxy server via TLS) +// */ +// tls?: boolean - /** - * Additional TLS options, used if `tls = true`. - * Can contain stuff like custom certificate, host, - * or whatever. - */ - tlsOptions?: SecureContextOptions -} +// /** +// * Additional TLS options, used if `tls = true`. +// * Can contain stuff like custom certificate, host, +// * or whatever. +// */ +// tlsOptions?: SecureContextOptions +// } -/** - * TCP transport that connects via an HTTP(S) proxy. - */ -export abstract class BaseHttpProxyTcpTransport extends BaseTcpTransport { - readonly _proxy: HttpProxySettings +// /** +// * TCP transport that connects via an HTTP(S) proxy. +// */ +// export abstract class BaseHttpProxyTcpTransport extends BaseTcpTransport { +// readonly _proxy: HttpProxySettings - constructor(proxy: HttpProxySettings) { - super() - this._proxy = proxy - } +// constructor(proxy: HttpProxySettings) { +// super() +// this._proxy = proxy +// } - private _platform = new NodePlatform() +// private _platform = new NodePlatform() - connect(dc: tl.RawDcOption): void { - if (this._state !== TransportState.Idle) { - throw new MtcuteError('Transport is not IDLE') - } +// connect(dc: tl.RawDcOption): void { +// if (this._state !== TransportState.Idle) { +// throw new MtcuteError('Transport is not IDLE') +// } - if (!this.packetCodecInitialized) { - this._packetCodec.on('error', err => this.emit('error', err)) - this._packetCodec.on('packet', buf => this.emit('message', buf)) - this.packetCodecInitialized = true - } +// if (!this.packetCodecInitialized) { +// this._packetCodec.on('error', err => this.emit('error', err)) +// this._packetCodec.on('packet', buf => this.emit('message', buf)) +// this.packetCodecInitialized = true +// } - this._state = TransportState.Connecting - this._currentDc = dc +// this._state = TransportState.Connecting +// this._currentDc = dc - this._socket = this._proxy.tls - ? connectTls(this._proxy.port, this._proxy.host, this._proxy.tlsOptions, this._onProxyConnected.bind(this)) - : connectTcp(this._proxy.port, this._proxy.host, this._onProxyConnected.bind(this)) +// this._socket = this._proxy.tls +// ? connectTls(this._proxy.port, this._proxy.host, this._proxy.tlsOptions, this._onProxyConnected.bind(this)) +// : connectTcp(this._proxy.port, this._proxy.host, this._onProxyConnected.bind(this)) - this._socket.on('error', this.handleError.bind(this)) - this._socket.on('close', this.close.bind(this)) - } +// this._socket.on('error', this.handleError.bind(this)) +// this._socket.on('close', this.close.bind(this)) +// } - private _onProxyConnected() { - this.log.debug('[%s:%d] connected to proxy, sending CONNECT', this._proxy.host, this._proxy.port) +// private _onProxyConnected() { +// this.log.debug('[%s:%d] connected to proxy, sending CONNECT', this._proxy.host, this._proxy.port) - let ip = `${this._currentDc!.ipAddress}:${this._currentDc!.port}` - if (this._currentDc!.ipv6) ip = `[${ip}]` +// let ip = `${this._currentDc!.ipAddress}:${this._currentDc!.port}` +// if (this._currentDc!.ipv6) ip = `[${ip}]` - const headers = { - ...(this._proxy.headers ?? {}), - } - headers.Host = ip +// const headers = { +// ...(this._proxy.headers ?? {}), +// } +// headers.Host = ip - if (this._proxy.user) { - let auth = this._proxy.user +// if (this._proxy.user) { +// let auth = this._proxy.user - if (this._proxy.password) { - auth += `:${this._proxy.password}` - } - headers['Proxy-Authorization'] = `Basic ${this._platform.base64Encode(this._platform.utf8Encode(auth))}` - } - headers['Proxy-Connection'] = 'Keep-Alive' +// if (this._proxy.password) { +// auth += `:${this._proxy.password}` +// } +// headers['Proxy-Authorization'] = `Basic ${this._platform.base64Encode(this._platform.utf8Encode(auth))}` +// } +// headers['Proxy-Connection'] = 'Keep-Alive' - const headersStr = Object.keys(headers) - .map(k => `\r\n${k}: ${headers[k]}`) - .join('') - const packet = `CONNECT ${ip} HTTP/1.1${headersStr}\r\n\r\n` +// const headersStr = Object.keys(headers) +// .map(k => `\r\n${k}: ${headers[k]}`) +// .join('') +// const packet = `CONNECT ${ip} HTTP/1.1${headersStr}\r\n\r\n` - this._socket!.write(packet) - this._socket!.once('data', (msg) => { - this.log.debug('[%s:%d] CONNECT resulted in: %s', this._proxy.host, this._proxy.port, msg) +// this._socket!.write(packet) +// this._socket!.once('data', (msg) => { +// this.log.debug('[%s:%d] CONNECT resulted in: %s', this._proxy.host, this._proxy.port, msg) - const [proto, code, name] = msg.toString().split(' ') +// const [proto, code, name] = msg.toString().split(' ') - if (!proto.match(/^HTTP\/1.[01]$/i)) { - // wtf? - this._socket!.emit( - 'error', - new HttpProxyConnectionError(this._proxy, `Server returned invalid protocol: ${proto}`), - ) +// if (!proto.match(/^HTTP\/1.[01]$/i)) { +// // wtf? +// this._socket!.emit( +// 'error', +// new HttpProxyConnectionError(this._proxy, `Server returned invalid protocol: ${proto}`), +// ) - return - } +// return +// } - if (code[0] !== '2') { - this._socket!.emit( - 'error', - new HttpProxyConnectionError(this._proxy, `Server returned error: ${code} ${name}`), - ) +// if (code[0] !== '2') { +// this._socket!.emit( +// 'error', +// new HttpProxyConnectionError(this._proxy, `Server returned error: ${code} ${name}`), +// ) - return - } +// return +// } - // all ok, connection established, can now call handleConnect - this._socket!.on('data', data => this._packetCodec.feed(data)) - this.handleConnect() - }) - } -} +// // all ok, connection established, can now call handleConnect +// this._socket!.on('data', data => this._packetCodec.feed(data)) +// this.handleConnect() +// }) +// } +// } -/** - * HTTP(s) TCP transport using an intermediate packet codec. - * - * Should be the one passed as `transport` to `TelegramClient` constructor - * (unless you want to use a custom codec). - */ -export class HttpProxyTcpTransport extends BaseHttpProxyTcpTransport { - _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec() -} +// /** +// * HTTP(s) TCP transport using an intermediate packet codec. +// * +// * Should be the one passed as `transport` to `TelegramClient` constructor +// * (unless you want to use a custom codec). +// */ +// export class HttpProxyTcpTransport extends BaseHttpProxyTcpTransport { +// _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec() +// } diff --git a/packages/mtproxy/fake-tls.ts b/packages/mtproxy/fake-tls.ts index 2c379260..df0618ba 100644 --- a/packages/mtproxy/fake-tls.ts +++ b/packages/mtproxy/fake-tls.ts @@ -1,351 +1,352 @@ -/* eslint-disable no-restricted-globals */ -import type { IPacketCodec } from '@mtcute/node' -import { WrappedCodec } from '@mtcute/node' -import type { ICryptoProvider } from '@mtcute/node/utils.js' -import { bigIntModInv, bigIntModPow, bigIntToBuffer, bufferToBigInt } from '@mtcute/node/utils.js' +// /* eslint-disable no-restricted-globals */ +// todo fixme +// import type { IPacketCodec } from '@mtcute/node' +// import { WrappedCodec } from '@mtcute/node' +// import type { ICryptoProvider } from '@mtcute/node/utils.js' +// import { bigIntModInv, bigIntModPow, bigIntToBuffer, bufferToBigInt } from '@mtcute/node/utils.js' -const MAX_TLS_PACKET_LENGTH = 2878 -const TLS_FIRST_PREFIX = Buffer.from('140303000101', 'hex') +// const MAX_TLS_PACKET_LENGTH = 2878 +// const TLS_FIRST_PREFIX = Buffer.from('140303000101', 'hex') -// ref: https://github.com/tdlib/td/blob/master/td/mtproto/TlsInit.cpp -const KEY_MOD = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEDn -// 2^255 - 19 -const QUAD_RES_MOD = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEDn -// (mod - 1) / 2 = 2^254 - 10 -const QUAD_RES_POW = 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6n +// // ref: https://github.com/tdlib/td/blob/master/td/mtproto/TlsInit.cpp +// const KEY_MOD = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEDn +// // 2^255 - 19 +// const QUAD_RES_MOD = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEDn +// // (mod - 1) / 2 = 2^254 - 10 +// const QUAD_RES_POW = 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6n -function _getY2(x: bigint, mod: bigint): bigint { - // returns y = x^3 + x^2 * 486662 + x - let y = x - y = (y + 486662n) % mod - y = (y * x) % mod - y = (y + 1n) % mod - y = (y * x) % mod +// function _getY2(x: bigint, mod: bigint): bigint { +// // returns y = x^3 + x^2 * 486662 + x +// let y = x +// y = (y + 486662n) % mod +// y = (y * x) % mod +// y = (y + 1n) % mod +// y = (y * x) % mod - return y -} +// return y +// } -function _getDoubleX(x: bigint, mod: bigint): bigint { - // returns x_2 = (x^2 - 1)^2/(4*y^2) - let denominator = _getY2(x, mod) - denominator = (denominator * 4n) % mod +// function _getDoubleX(x: bigint, mod: bigint): bigint { +// // returns x_2 = (x^2 - 1)^2/(4*y^2) +// let denominator = _getY2(x, mod) +// denominator = (denominator * 4n) % mod - let numerator = (x * x) % mod - numerator = (numerator - 1n) % mod - numerator = (numerator * numerator) % mod +// let numerator = (x * x) % mod +// numerator = (numerator - 1n) % mod +// numerator = (numerator * numerator) % mod - denominator = bigIntModInv(denominator, mod) - numerator = (numerator * denominator) % mod +// denominator = bigIntModInv(denominator, mod) +// numerator = (numerator * denominator) % mod - return numerator -} +// return numerator +// } -function _isQuadraticResidue(a: bigint): boolean { - const r = bigIntModPow(a, QUAD_RES_POW, QUAD_RES_MOD) +// function _isQuadraticResidue(a: bigint): boolean { +// const r = bigIntModPow(a, QUAD_RES_POW, QUAD_RES_MOD) - return r === 1n -} +// return r === 1n +// } -interface TlsOperationHandler { - string: (buf: Buffer) => void - zero: (size: number) => void - random: (size: number) => void - domain: () => void - grease: (seed: number) => void - beginScope: () => void - endScope: () => void - key: () => void -} +// interface TlsOperationHandler { +// string: (buf: Buffer) => void +// zero: (size: number) => void +// random: (size: number) => void +// domain: () => void +// grease: (seed: number) => void +// beginScope: () => void +// endScope: () => void +// key: () => void +// } -function executeTlsOperations(h: TlsOperationHandler): void { - h.string(Buffer.from('1603010200010001fc0303', 'hex')) - h.zero(32) - h.string(Buffer.from('20', 'hex')) - h.random(32) - h.string(Buffer.from('0020', 'hex')) - h.grease(0) - h.string(Buffer.from('130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f003501000193', 'hex')) - h.grease(2) - h.string(Buffer.from('00000000', 'hex')) - h.beginScope() - h.beginScope() - h.string(Buffer.from('00', 'hex')) - h.beginScope() - h.domain() - h.endScope() - h.endScope() - h.endScope() - h.string(Buffer.from('00170000ff01000100000a000a0008', 'hex')) - h.grease(4) - h.string( - Buffer.from( - '001d00170018000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0012001004030804040105030805050108060601001200000033002b0029', - 'hex', - ), - ) - h.grease(4) - h.string(Buffer.from('000100001d0020', 'hex')) - h.key() - h.string(Buffer.from('002d00020101002b000b0a', 'hex')) - h.grease(6) - h.string(Buffer.from('0304030303020301001b0003020002', 'hex')) - h.grease(3) - h.string(Buffer.from('0001000015', 'hex')) -} +// function executeTlsOperations(h: TlsOperationHandler): void { +// h.string(Buffer.from('1603010200010001fc0303', 'hex')) +// h.zero(32) +// h.string(Buffer.from('20', 'hex')) +// h.random(32) +// h.string(Buffer.from('0020', 'hex')) +// h.grease(0) +// h.string(Buffer.from('130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f003501000193', 'hex')) +// h.grease(2) +// h.string(Buffer.from('00000000', 'hex')) +// h.beginScope() +// h.beginScope() +// h.string(Buffer.from('00', 'hex')) +// h.beginScope() +// h.domain() +// h.endScope() +// h.endScope() +// h.endScope() +// h.string(Buffer.from('00170000ff01000100000a000a0008', 'hex')) +// h.grease(4) +// h.string( +// Buffer.from( +// '001d00170018000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0012001004030804040105030805050108060601001200000033002b0029', +// 'hex', +// ), +// ) +// h.grease(4) +// h.string(Buffer.from('000100001d0020', 'hex')) +// h.key() +// h.string(Buffer.from('002d00020101002b000b0a', 'hex')) +// h.grease(6) +// h.string(Buffer.from('0304030303020301001b0003020002', 'hex')) +// h.grease(3) +// h.string(Buffer.from('0001000015', 'hex')) +// } + +// // i dont know why is this needed, since it is always padded to 517 bytes +// // this was in tdlib sources, so whatever. not used here though, and works just fine +// // class TlsHelloCounter implements TlsOperationHandler { +// // size = 0 +// // +// // private _domain: Buffer +// // +// // constructor(domain: Buffer) { +// // this._domain = domain +// // } +// // +// // string(buf: Buffer) { +// // this.size += buf.length +// // } +// // +// // random(size: number) { +// // this.size += size +// // } +// // +// // zero(size: number) { +// // this.size += size +// // } +// // +// // domain() { +// // this.size += this._domain.length +// // } +// // +// // grease() { +// // this.size += 2 +// // } +// // +// // key() { +// // this.size += 32 +// // } +// // +// // beginScope() { +// // this.size += 2 +// // } +// // +// // endScope() { +// // // no-op, since this does not affect size +// // } +// // +// // finish(): number { +// // const zeroPad = 515 - this.size +// // this.beginScope() +// // this.zero(zeroPad) +// // this.endScope() +// // +// // return this.size +// // } +// // } + +// function initGrease(crypto: ICryptoProvider, size: number): Buffer { +// const buf = crypto.randomBytes(size) + +// for (let i = 0; i < size; i++) { +// buf[i] = (buf[i] & 0xF0) + 0x0A +// } + +// for (let i = 1; i < size; i += 2) { +// if (buf[i] === buf[i - 1]) { +// buf[i] ^= 0x10 +// } +// } + +// return Buffer.from(buf) +// } + +// class TlsHelloWriter implements TlsOperationHandler { +// buf: Buffer +// pos = 0 -// i dont know why is this needed, since it is always padded to 517 bytes -// this was in tdlib sources, so whatever. not used here though, and works just fine -// class TlsHelloCounter implements TlsOperationHandler { -// size = 0 -// // private _domain: Buffer -// -// constructor(domain: Buffer) { +// private _grease +// private _scopes: number[] = [] + +// constructor( +// readonly crypto: ICryptoProvider, +// size: number, +// domain: Buffer, +// ) { // this._domain = domain +// this.buf = Buffer.allocUnsafe(size) +// this._grease = initGrease(this.crypto, 7) // } -// + // string(buf: Buffer) { -// this.size += buf.length +// buf.copy(this.buf, this.pos) +// this.pos += buf.length // } -// + // random(size: number) { -// this.size += size +// this.string(Buffer.from(this.crypto.randomBytes(size))) // } -// + // zero(size: number) { -// this.size += size +// this.string(Buffer.alloc(size, 0)) // } -// + // domain() { -// this.size += this._domain.length +// this.string(this._domain) // } -// -// grease() { -// this.size += 2 + +// grease(seed: number) { +// this.buf[this.pos] = this.buf[this.pos + 1] = this._grease[seed] +// this.pos += 2 // } -// + // key() { -// this.size += 32 +// for (;;) { +// const key = this.crypto.randomBytes(32) +// key[31] &= 127 + +// let x = bufferToBigInt(key) +// const y = _getY2(x, KEY_MOD) + +// if (_isQuadraticResidue(y)) { +// for (let i = 0; i < 3; i++) { +// x = _getDoubleX(x, KEY_MOD) +// } + +// const key = bigIntToBuffer(x, 32, true) +// this.string(Buffer.from(key)) + +// return +// } +// } // } -// + // beginScope() { -// this.size += 2 +// this._scopes.push(this.pos) +// this.pos += 2 // } -// + // endScope() { -// // no-op, since this does not affect size +// const begin = this._scopes.pop() + +// if (begin === undefined) { +// throw new Error('endScope called without beginScope') +// } + +// const end = this.pos +// const size = end - begin - 2 + +// this.buf.writeUInt16BE(size, begin) // } -// -// finish(): number { -// const zeroPad = 515 - this.size + +// async finish(secret: Buffer): Promise { +// const padSize = 515 - this.pos +// const unixTime = ~~(Date.now() / 1000) + // this.beginScope() -// this.zero(zeroPad) +// this.zero(padSize) // this.endScope() -// -// return this.size + +// const hash = Buffer.from(await this.crypto.hmacSha256(this.buf, secret)) + +// const old = hash.readInt32LE(28) +// hash.writeInt32LE(old ^ unixTime, 28) + +// hash.copy(this.buf, 11) + +// return this.buf // } // } -function initGrease(crypto: ICryptoProvider, size: number): Buffer { - const buf = crypto.randomBytes(size) +// /** @internal */ +// export async function generateFakeTlsHeader(domain: string, secret: Buffer, crypto: ICryptoProvider): Promise { +// const domainBuf = Buffer.from(domain) - for (let i = 0; i < size; i++) { - buf[i] = (buf[i] & 0xF0) + 0x0A - } +// const writer = new TlsHelloWriter(crypto, 517, domainBuf) +// executeTlsOperations(writer) - for (let i = 1; i < size; i += 2) { - if (buf[i] === buf[i - 1]) { - buf[i] ^= 0x10 - } - } +// return writer.finish(secret) +// } - return Buffer.from(buf) -} +// /** +// * Fake TLS packet codec, used for some MTProxies. +// * +// * Must only be used inside {@link MtProxyTcpTransport} +// * @internal +// */ +// export class FakeTlsPacketCodec extends WrappedCodec implements IPacketCodec { +// protected _stream: Buffer = Buffer.alloc(0) -class TlsHelloWriter implements TlsOperationHandler { - buf: Buffer - pos = 0 +// private _header!: Buffer +// private _isFirstTls = true - private _domain: Buffer - private _grease - private _scopes: number[] = [] +// async tag(): Promise { +// this._header = Buffer.from(await this._inner.tag()) - constructor( - readonly crypto: ICryptoProvider, - size: number, - domain: Buffer, - ) { - this._domain = domain - this.buf = Buffer.allocUnsafe(size) - this._grease = initGrease(this.crypto, 7) - } +// return Buffer.alloc(0) +// } - string(buf: Buffer) { - buf.copy(this.buf, this.pos) - this.pos += buf.length - } +// private _encodeTls(packet: Buffer): Buffer { +// if (this._header.length) { +// packet = Buffer.concat([this._header, packet]) +// this._header = Buffer.alloc(0) +// } - random(size: number) { - this.string(Buffer.from(this.crypto.randomBytes(size))) - } +// const header = Buffer.from([0x17, 0x03, 0x03, 0x00, 0x00]) +// header.writeUInt16BE(packet.length, 3) - zero(size: number) { - this.string(Buffer.alloc(size, 0)) - } +// if (this._isFirstTls) { +// this._isFirstTls = false - domain() { - this.string(this._domain) - } +// return Buffer.concat([TLS_FIRST_PREFIX, header, packet]) +// } - grease(seed: number) { - this.buf[this.pos] = this.buf[this.pos + 1] = this._grease[seed] - this.pos += 2 - } +// return Buffer.concat([header, packet]) +// } - key() { - for (;;) { - const key = this.crypto.randomBytes(32) - key[31] &= 127 +// async encode(packet: Buffer): Promise { +// packet = Buffer.from(await this._inner.encode(packet)) - let x = bufferToBigInt(key) - const y = _getY2(x, KEY_MOD) +// if (packet.length + this._header.length > MAX_TLS_PACKET_LENGTH) { +// const ret: Buffer[] = [] - if (_isQuadraticResidue(y)) { - for (let i = 0; i < 3; i++) { - x = _getDoubleX(x, KEY_MOD) - } +// while (packet.length) { +// const buf = packet.slice(0, MAX_TLS_PACKET_LENGTH - this._header.length) +// packet = packet.slice(buf.length) +// ret.push(this._encodeTls(buf)) +// } - const key = bigIntToBuffer(x, 32, true) - this.string(Buffer.from(key)) +// return Buffer.concat(ret) +// } - return - } - } - } +// return this._encodeTls(packet) +// } - beginScope() { - this._scopes.push(this.pos) - this.pos += 2 - } +// feed(data: Buffer): void { +// this._stream = Buffer.concat([this._stream, data]) - endScope() { - const begin = this._scopes.pop() +// for (;;) { +// if (this._stream.length < 5) return - if (begin === undefined) { - throw new Error('endScope called without beginScope') - } +// if (!(this._stream[0] === 0x17 && this._stream[1] === 0x03 && this._stream[2] === 0x03)) { +// this.emit('error', new Error('Invalid TLS header')) - const end = this.pos - const size = end - begin - 2 +// return +// } - this.buf.writeUInt16BE(size, begin) - } +// const length = this._stream.readUInt16BE(3) +// if (length < this._stream.length - 5) return - async finish(secret: Buffer): Promise { - const padSize = 515 - this.pos - const unixTime = ~~(Date.now() / 1000) +// this._inner.feed(this._stream.slice(5, length + 5)) +// this._stream = this._stream.slice(length + 5) +// } +// } - this.beginScope() - this.zero(padSize) - this.endScope() - - const hash = Buffer.from(await this.crypto.hmacSha256(this.buf, secret)) - - const old = hash.readInt32LE(28) - hash.writeInt32LE(old ^ unixTime, 28) - - hash.copy(this.buf, 11) - - return this.buf - } -} - -/** @internal */ -export async function generateFakeTlsHeader(domain: string, secret: Buffer, crypto: ICryptoProvider): Promise { - const domainBuf = Buffer.from(domain) - - const writer = new TlsHelloWriter(crypto, 517, domainBuf) - executeTlsOperations(writer) - - return writer.finish(secret) -} - -/** - * Fake TLS packet codec, used for some MTProxies. - * - * Must only be used inside {@link MtProxyTcpTransport} - * @internal - */ -export class FakeTlsPacketCodec extends WrappedCodec implements IPacketCodec { - protected _stream: Buffer = Buffer.alloc(0) - - private _header!: Buffer - private _isFirstTls = true - - async tag(): Promise { - this._header = Buffer.from(await this._inner.tag()) - - return Buffer.alloc(0) - } - - private _encodeTls(packet: Buffer): Buffer { - if (this._header.length) { - packet = Buffer.concat([this._header, packet]) - this._header = Buffer.alloc(0) - } - - const header = Buffer.from([0x17, 0x03, 0x03, 0x00, 0x00]) - header.writeUInt16BE(packet.length, 3) - - if (this._isFirstTls) { - this._isFirstTls = false - - return Buffer.concat([TLS_FIRST_PREFIX, header, packet]) - } - - return Buffer.concat([header, packet]) - } - - async encode(packet: Buffer): Promise { - packet = Buffer.from(await this._inner.encode(packet)) - - if (packet.length + this._header.length > MAX_TLS_PACKET_LENGTH) { - const ret: Buffer[] = [] - - while (packet.length) { - const buf = packet.slice(0, MAX_TLS_PACKET_LENGTH - this._header.length) - packet = packet.slice(buf.length) - ret.push(this._encodeTls(buf)) - } - - return Buffer.concat(ret) - } - - return this._encodeTls(packet) - } - - feed(data: Buffer): void { - this._stream = Buffer.concat([this._stream, data]) - - for (;;) { - if (this._stream.length < 5) return - - if (!(this._stream[0] === 0x17 && this._stream[1] === 0x03 && this._stream[2] === 0x03)) { - this.emit('error', new Error('Invalid TLS header')) - - return - } - - const length = this._stream.readUInt16BE(3) - if (length < this._stream.length - 5) return - - this._inner.feed(this._stream.slice(5, length + 5)) - this._stream = this._stream.slice(length + 5) - } - } - - reset(): void { - this._stream = Buffer.alloc(0) - this._isFirstTls = true - } -} +// reset(): void { +// this._stream = Buffer.alloc(0) +// this._isFirstTls = true +// } +// } diff --git a/packages/mtproxy/index.ts b/packages/mtproxy/index.ts index 3dd5a548..99d70912 100644 --- a/packages/mtproxy/index.ts +++ b/packages/mtproxy/index.ts @@ -1,250 +1,250 @@ -/* eslint-disable no-restricted-globals */ +// /* eslint-disable no-restricted-globals */ // todo fixme -import { connect } from 'node:net' +// import { connect } from 'node:net' -import type { - IPacketCodec, - tl, -} from '@mtcute/node' -import { - BaseTcpTransport, - IntermediatePacketCodec, - MtSecurityError, - MtUnsupportedError, - MtcuteError, - ObfuscatedPacketCodec, - PaddedIntermediatePacketCodec, - TransportState, -} from '@mtcute/node' -import { buffersEqual } from '@mtcute/node/utils.js' +// import type { +// IPacketCodec, +// tl, +// } from '@mtcute/node' +// import { +// BaseTcpTransport, +// IntermediatePacketCodec, +// MtSecurityError, +// MtUnsupportedError, +// MtcuteError, +// ObfuscatedPacketCodec, +// PaddedIntermediatePacketCodec, +// TransportState, +// } from '@mtcute/node' +// import { buffersEqual } from '@mtcute/node/utils.js' -import { FakeTlsPacketCodec, generateFakeTlsHeader } from './fake-tls.js' +// import { FakeTlsPacketCodec, generateFakeTlsHeader } from './fake-tls.js' -/** - * MTProto proxy settings - */ -export interface MtProxySettings { - /** - * Host or IP of the proxy (e.g. `proxy.example.com`, `1.2.3.4`) - */ - host: string +// /** +// * MTProto proxy settings +// */ +// export interface MtProxySettings { +// /** +// * Host or IP of the proxy (e.g. `proxy.example.com`, `1.2.3.4`) +// */ +// host: string - /** - * Port of the proxy (e.g. `8888`) - */ - port: number +// /** +// * Port of the proxy (e.g. `8888`) +// */ +// port: number - /** - * Secret of the proxy, optionally encoded either as hex or base64 - */ - secret: string | Buffer -} +// /** +// * Secret of the proxy, optionally encoded either as hex or base64 +// */ +// secret: string | Buffer +// } -const MAX_DOMAIN_LENGTH = 182 // must be small enough not to overflow TLS-hello length -const TLS_START = [Buffer.from('160303', 'hex'), Buffer.from('140303000101170303', 'hex')] +// const MAX_DOMAIN_LENGTH = 182 // must be small enough not to overflow TLS-hello length +// const TLS_START = [Buffer.from('160303', 'hex'), Buffer.from('140303000101170303', 'hex')] -/** - * TCP transport that connects via an MTProxy - */ -export class MtProxyTcpTransport extends BaseTcpTransport { - readonly _proxy: MtProxySettings +// /** +// * TCP transport that connects via an MTProxy +// */ +// export class MtProxyTcpTransport extends BaseTcpTransport { +// readonly _proxy: MtProxySettings - private _rawSecret: Buffer - private _randomPadding = false - private _fakeTlsDomain: string | null = null +// private _rawSecret: Buffer +// private _randomPadding = false +// private _fakeTlsDomain: string | null = null - /** - * @param proxy Information about the proxy - */ - constructor(proxy: MtProxySettings) { - super() +// /** +// * @param proxy Information about the proxy +// */ +// constructor(proxy: MtProxySettings) { +// super() - this._proxy = proxy +// this._proxy = proxy - // validate and parse secret - let secret: Buffer +// // validate and parse secret +// let secret: Buffer - if (Buffer.isBuffer(proxy.secret)) { - secret = proxy.secret - } else if (proxy.secret.match(/^[0-9a-f]+$/i)) { - secret = Buffer.from(proxy.secret, 'hex') - } else { - secret = Buffer.from(proxy.secret, 'base64url') - } +// if (Buffer.isBuffer(proxy.secret)) { +// secret = proxy.secret +// } else if (proxy.secret.match(/^[0-9a-f]+$/i)) { +// secret = Buffer.from(proxy.secret, 'hex') +// } else { +// secret = Buffer.from(proxy.secret, 'base64url') +// } - if (secret.length > 17 + MAX_DOMAIN_LENGTH) { - throw new MtSecurityError('Invalid secret: too long') - } +// if (secret.length > 17 + MAX_DOMAIN_LENGTH) { +// throw new MtSecurityError('Invalid secret: too long') +// } - if (secret.length < 16) { - throw new MtSecurityError('Invalid secret: too short') - } +// if (secret.length < 16) { +// throw new MtSecurityError('Invalid secret: too short') +// } - if (secret.length === 16) { - this._rawSecret = secret - } else if (secret.length === 17 && secret[0] === 0xDD) { - this._rawSecret = secret.slice(1) - this._randomPadding = true - } else if (secret.length >= 18 && secret[0] === 0xEE) { - this._rawSecret = secret.slice(1, 17) - this._fakeTlsDomain = secret.slice(17).toString() - } else { - throw new MtUnsupportedError('Unsupported secret') - } - } +// if (secret.length === 16) { +// this._rawSecret = secret +// } else if (secret.length === 17 && secret[0] === 0xDD) { +// this._rawSecret = secret.slice(1) +// this._randomPadding = true +// } else if (secret.length >= 18 && secret[0] === 0xEE) { +// this._rawSecret = secret.slice(1, 17) +// this._fakeTlsDomain = secret.slice(17).toString() +// } else { +// throw new MtUnsupportedError('Unsupported secret') +// } +// } - getMtproxyInfo(): tl.RawInputClientProxy { - return { - _: 'inputClientProxy', - address: this._proxy.host, - port: this._proxy.port, - } - } +// getMtproxyInfo(): tl.RawInputClientProxy { +// return { +// _: 'inputClientProxy', +// address: this._proxy.host, +// port: this._proxy.port, +// } +// } - _packetCodec!: IPacketCodec +// _packetCodec!: IPacketCodec - connect(dc: tl.RawDcOption, testMode: boolean): void { - if (this._state !== TransportState.Idle) { - throw new MtcuteError('Transport is not IDLE') - } +// connect(dc: tl.RawDcOption, testMode: boolean): void { +// if (this._state !== TransportState.Idle) { +// throw new MtcuteError('Transport is not IDLE') +// } - if (this._packetCodec && this._currentDc?.id !== dc.id) { - // dc changed, thus the codec's init will change too - // clean up to avoid memory leaks - this.packetCodecInitialized = false - this._packetCodec.reset() - this._packetCodec.removeAllListeners() - delete (this as Partial)._packetCodec - } +// if (this._packetCodec && this._currentDc?.id !== dc.id) { +// // dc changed, thus the codec's init will change too +// // clean up to avoid memory leaks +// this.packetCodecInitialized = false +// this._packetCodec.reset() +// this._packetCodec.removeAllListeners() +// delete (this as Partial)._packetCodec +// } - if (!this._packetCodec) { - const proxy = { - dcId: dc.id, - media: dc.mediaOnly!, - test: testMode, - secret: this._rawSecret, - } +// if (!this._packetCodec) { +// const proxy = { +// dcId: dc.id, +// media: dc.mediaOnly!, +// test: testMode, +// secret: this._rawSecret, +// } - if (!this._fakeTlsDomain) { - let inner: IPacketCodec +// if (!this._fakeTlsDomain) { +// let inner: IPacketCodec - if (this._randomPadding) { - inner = new PaddedIntermediatePacketCodec() - } else { - inner = new IntermediatePacketCodec() - } +// if (this._randomPadding) { +// inner = new PaddedIntermediatePacketCodec() +// } else { +// inner = new IntermediatePacketCodec() +// } - this._packetCodec = new ObfuscatedPacketCodec(inner, proxy) - } else { - this._packetCodec = new FakeTlsPacketCodec( - new ObfuscatedPacketCodec(new PaddedIntermediatePacketCodec(), proxy), - ) - } +// this._packetCodec = new ObfuscatedPacketCodec(inner, proxy) +// } else { +// this._packetCodec = new FakeTlsPacketCodec( +// new ObfuscatedPacketCodec(new PaddedIntermediatePacketCodec(), proxy), +// ) +// } - this._packetCodec.setup?.(this._crypto, this.log) - this._packetCodec.on('error', err => this.emit('error', err)) - this._packetCodec.on('packet', buf => this.emit('message', buf)) - } +// this._packetCodec.setup?.(this._crypto, this.log) +// this._packetCodec.on('error', err => this.emit('error', err)) +// this._packetCodec.on('packet', buf => this.emit('message', buf)) +// } - this._state = TransportState.Connecting - this._currentDc = dc +// this._state = TransportState.Connecting +// this._currentDc = dc - if (this._fakeTlsDomain) { - this._socket = connect( - this._proxy.port, - this._proxy.host, - // MTQ-55 - // eslint-disable-next-line ts/no-misused-promises - this._handleConnectFakeTls.bind(this), - ) - } else { - this._socket = connect( - this._proxy.port, - this._proxy.host, - // MTQ-55 +// if (this._fakeTlsDomain) { +// this._socket = connect( +// this._proxy.port, +// this._proxy.host, +// // MTQ-55 +// // eslint-disable-next-line ts/no-misused-promises +// this._handleConnectFakeTls.bind(this), +// ) +// } else { +// this._socket = connect( +// this._proxy.port, +// this._proxy.host, +// // MTQ-55 - this.handleConnect.bind(this), - ) - this._socket.on('data', data => this._packetCodec.feed(data)) - } - this._socket.on('error', this.handleError.bind(this)) - this._socket.on('close', this.close.bind(this)) - } +// this.handleConnect.bind(this), +// ) +// this._socket.on('data', data => this._packetCodec.feed(data)) +// } +// this._socket.on('error', this.handleError.bind(this)) +// this._socket.on('close', this.close.bind(this)) +// } - private async _handleConnectFakeTls(): Promise { - try { - const hello = await generateFakeTlsHeader(this._fakeTlsDomain!, this._rawSecret, this._crypto) - const helloRand = hello.slice(11, 11 + 32) +// private async _handleConnectFakeTls(): Promise { +// try { +// const hello = await generateFakeTlsHeader(this._fakeTlsDomain!, this._rawSecret, this._crypto) +// const helloRand = hello.slice(11, 11 + 32) - let serverHelloBuffer: Buffer | null = null +// let serverHelloBuffer: Buffer | null = null - const checkHelloResponse = async (buf: Buffer): Promise => { - if (serverHelloBuffer) { - buf = Buffer.concat([serverHelloBuffer, buf]) - } +// const checkHelloResponse = async (buf: Buffer): Promise => { +// if (serverHelloBuffer) { +// buf = Buffer.concat([serverHelloBuffer, buf]) +// } - const resp = buf +// const resp = buf - for (const first of TLS_START) { - if (buf.length < first.length + 2) { - throw new MtSecurityError('Server hello is too short') - } +// for (const first of TLS_START) { +// if (buf.length < first.length + 2) { +// throw new MtSecurityError('Server hello is too short') +// } - if (!buffersEqual(buf.slice(0, first.length), first)) { - throw new MtSecurityError('Server hello is invalid') - } - buf = buf.slice(first.length) +// if (!buffersEqual(buf.slice(0, first.length), first)) { +// throw new MtSecurityError('Server hello is invalid') +// } +// buf = buf.slice(first.length) - const skipSize = buf.readUInt16BE() - buf = buf.slice(2) +// const skipSize = buf.readUInt16BE() +// buf = buf.slice(2) - if (buf.length < skipSize) { - // likely got split into multiple packets - if (serverHelloBuffer) { - throw new MtSecurityError('Server hello is too short') - } +// if (buf.length < skipSize) { +// // likely got split into multiple packets +// if (serverHelloBuffer) { +// throw new MtSecurityError('Server hello is too short') +// } - serverHelloBuffer = resp +// serverHelloBuffer = resp - return false - } +// return false +// } - buf = buf.slice(skipSize) - } +// buf = buf.slice(skipSize) +// } - const respRand = resp.slice(11, 11 + 32) - const hash = await this._crypto.hmacSha256( - Buffer.concat([helloRand, resp.slice(0, 11), Buffer.alloc(32, 0), resp.slice(11 + 32)]), - this._rawSecret, - ) +// const respRand = resp.slice(11, 11 + 32) +// const hash = await this._crypto.hmacSha256( +// Buffer.concat([helloRand, resp.slice(0, 11), Buffer.alloc(32, 0), resp.slice(11 + 32)]), +// this._rawSecret, +// ) - if (!buffersEqual(hash, respRand)) { - throw new MtSecurityError('Response hash is invalid') - } +// if (!buffersEqual(hash, respRand)) { +// throw new MtSecurityError('Response hash is invalid') +// } - return true - } +// return true +// } - const packetHandler = (buf: Buffer): void => { - checkHelloResponse(buf) - .then((done) => { - if (!done) return +// const packetHandler = (buf: Buffer): void => { +// checkHelloResponse(buf) +// .then((done) => { +// if (!done) return - this._socket!.off('data', packetHandler) - this._socket!.on('data', (data) => { - this._packetCodec.feed(data) - }) +// this._socket!.off('data', packetHandler) +// this._socket!.on('data', (data) => { +// this._packetCodec.feed(data) +// }) - return this.handleConnect() - }) - .catch(err => this._socket!.emit('error', err)) - } +// return this.handleConnect() +// }) +// .catch(err => this._socket!.emit('error', err)) +// } - this._socket!.write(hello) - this._socket!.on('data', packetHandler) - } catch (e) { - this._socket!.emit('error', e) - } - } -} +// this._socket!.write(hello) +// this._socket!.on('data', packetHandler) +// } catch (e) { +// this._socket!.emit('error', e) +// } +// } +// } diff --git a/packages/node/package.json b/packages/node/package.json index 6b462a19..bcf9d515 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,30 +1,31 @@ { - "name": "@mtcute/node", - "type": "module", - "version": "0.16.13", - "private": true, - "description": "Meta-package for Node.js", - "author": "alina sireneva ", - "license": "MIT", - "sideEffects": false, - "exports": { - ".": "./src/index.ts", - "./utils.js": "./src/utils.ts", - "./methods.js": "./src/methods.ts" - }, - "scripts": { - "docs": "typedoc", - "build": "pnpm run -w build-package node" - }, - "dependencies": { - "@mtcute/core": "workspace:^", - "@mtcute/html-parser": "workspace:^", - "@mtcute/markdown-parser": "workspace:^", - "@mtcute/wasm": "workspace:^", - "better-sqlite3": "11.3.0" - }, - "devDependencies": { - "@mtcute/test": "workspace:^", - "@types/better-sqlite3": "7.6.4" - } + "name": "@mtcute/node", + "type": "module", + "version": "0.16.13", + "private": true, + "description": "Meta-package for Node.js", + "author": "alina sireneva ", + "license": "MIT", + "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./utils.js": "./src/utils.ts", + "./methods.js": "./src/methods.ts" + }, + "scripts": { + "docs": "typedoc", + "build": "pnpm run -w build-package node" + }, + "dependencies": { + "@mtcute/core": "workspace:^", + "@mtcute/html-parser": "workspace:^", + "@mtcute/markdown-parser": "workspace:^", + "@mtcute/wasm": "workspace:^", + "@fuman/node-net": "workspace:^", + "better-sqlite3": "11.3.0" + }, + "devDependencies": { + "@mtcute/test": "workspace:^", + "@types/better-sqlite3": "7.6.4" + } } diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index 59ba20d1..7c563f96 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -19,6 +19,7 @@ import { downloadAsNodeStream } from './methods/download-node-stream.js' import { SqliteStorage } from './sqlite/index.js' import { NodeCryptoProvider } from './utils/crypto.js' import { TcpTransport } from './utils/tcp.js' +// import { TcpTransport } from './utils/tcp.js' export type { TelegramClientOptions } @@ -60,7 +61,7 @@ export class BaseTelegramClient extends BaseTelegramClientBase { super({ // eslint-disable-next-line crypto: nativeCrypto ? new nativeCrypto() : new NodeCryptoProvider(), - transport: () => new TcpTransport(), + transport: TcpTransport, ...opts, storage: typeof opts.storage === 'string' diff --git a/packages/node/src/utils/tcp.test.ts b/packages/node/src/utils/tcp.test.ts index b42643bb..3c941abd 100644 --- a/packages/node/src/utils/tcp.test.ts +++ b/packages/node/src/utils/tcp.test.ts @@ -1,157 +1,158 @@ -import type { Socket } from 'node:net' +// import type { Socket } from 'node:net' -import type { MockedObject } from 'vitest' -import { describe, expect, it, vi } from 'vitest' -import { TransportState } from '@mtcute/core' -import { getPlatform } from '@mtcute/core/platform.js' -import { LogManager, defaultProductionDc } from '@mtcute/core/utils.js' +// import type { MockedObject } from 'vitest' +// import { describe, expect, it, vi } from 'vitest' +// import { TransportState } from '@mtcute/core' +// import { getPlatform } from '@mtcute/core/platform.js' +// import { LogManager, defaultProductionDc } from '@mtcute/core/utils.js' -if (import.meta.env.TEST_ENV === 'node') { - vi.doMock('net', () => ({ - connect: vi.fn().mockImplementation((port: number, ip: string, cb: () => void) => { - cb() +// todo: move to fuman +// if (import.meta.env.TEST_ENV === 'node') { +// vi.doMock('net', () => ({ +// connect: vi.fn().mockImplementation((port: number, ip: string, cb: () => void) => { +// cb() - return { - on: vi.fn(), - write: vi.fn().mockImplementation((data: Uint8Array, cb: () => void) => { - cb() - }), - end: vi.fn(), - removeAllListeners: vi.fn(), - destroy: vi.fn(), - } - }), - })) +// return { +// on: vi.fn(), +// write: vi.fn().mockImplementation((data: Uint8Array, cb: () => void) => { +// cb() +// }), +// end: vi.fn(), +// removeAllListeners: vi.fn(), +// destroy: vi.fn(), +// } +// }), +// })) - const net = await import('node:net') - const connect = vi.mocked(net.connect) +// const net = await import('node:net') +// const connect = vi.mocked(net.connect) - const { TcpTransport } = await import('./tcp.js') - const { defaultTestCryptoProvider, u8HexDecode } = await import('@mtcute/test') +// const { TcpTransport } = await import('./tcp.js') +// const { defaultTestCryptoProvider, u8HexDecode } = await import('@mtcute/test') - describe('TcpTransport', () => { - const getLastSocket = () => { - return connect.mock.results[connect.mock.results.length - 1].value as MockedObject - } +// describe('TcpTransport', () => { +// const getLastSocket = () => { +// return connect.mock.results[connect.mock.results.length - 1].value as MockedObject +// } - const create = async () => { - const transport = new TcpTransport() - const logger = new LogManager() - logger.level = 0 - transport.setup(await defaultTestCryptoProvider(), logger) +// const create = async () => { +// const transport = new TcpTransport() +// const logger = new LogManager() +// logger.level = 0 +// transport.setup(await defaultTestCryptoProvider(), logger) - return transport - } +// return transport +// } - it('should initiate a tcp connection to the given dc', async () => { - const t = await create() +// it('should initiate a tcp connection to the given dc', async () => { +// const t = await create() - t.connect(defaultProductionDc.main, false) +// t.connect(defaultProductionDc.main, false) - expect(connect).toHaveBeenCalledOnce() - expect(connect).toHaveBeenCalledWith( - defaultProductionDc.main.port, - defaultProductionDc.main.ipAddress, - expect.any(Function), - ) - await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - }) +// expect(connect).toHaveBeenCalledOnce() +// expect(connect).toHaveBeenCalledWith( +// defaultProductionDc.main.port, +// defaultProductionDc.main.ipAddress, +// expect.any(Function), +// ) +// await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) +// }) - it('should set up event handlers', async () => { - const t = await create() +// it('should set up event handlers', async () => { +// const t = await create() - t.connect(defaultProductionDc.main, false) +// t.connect(defaultProductionDc.main, false) - const socket = getLastSocket() +// const socket = getLastSocket() - expect(socket.on).toHaveBeenCalledTimes(3) - expect(socket.on).toHaveBeenCalledWith('data', expect.any(Function)) - expect(socket.on).toHaveBeenCalledWith('error', expect.any(Function)) - expect(socket.on).toHaveBeenCalledWith('close', expect.any(Function)) - }) +// expect(socket.on).toHaveBeenCalledTimes(3) +// expect(socket.on).toHaveBeenCalledWith('data', expect.any(Function)) +// expect(socket.on).toHaveBeenCalledWith('error', expect.any(Function)) +// expect(socket.on).toHaveBeenCalledWith('close', expect.any(Function)) +// }) - it('should write packet codec tag once connected', async () => { - const t = await create() +// it('should write packet codec tag once connected', async () => { +// const t = await create() - t.connect(defaultProductionDc.main, false) +// t.connect(defaultProductionDc.main, false) - const socket = getLastSocket() +// const socket = getLastSocket() - await vi.waitFor(() => - expect(socket.write).toHaveBeenCalledWith( - u8HexDecode('eeeeeeee'), // intermediate - expect.any(Function), - ), - ) - }) +// await vi.waitFor(() => +// expect(socket.write).toHaveBeenCalledWith( +// u8HexDecode('eeeeeeee'), // intermediate +// expect.any(Function), +// ), +// ) +// }) - it('should write to the underlying socket', async () => { - const t = await create() +// it('should write to the underlying socket', async () => { +// const t = await create() - t.connect(defaultProductionDc.main, false) - await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) +// t.connect(defaultProductionDc.main, false) +// await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - await t.send(getPlatform().hexDecode('00010203040506070809')) +// await t.send(getPlatform().hexDecode('00010203040506070809')) - const socket = getLastSocket() +// const socket = getLastSocket() - expect(socket.write).toHaveBeenCalledWith(u8HexDecode('0a00000000010203040506070809'), expect.any(Function)) - }) +// expect(socket.write).toHaveBeenCalledWith(u8HexDecode('0a00000000010203040506070809'), expect.any(Function)) +// }) - it('should correctly close', async () => { - const t = await create() +// it('should correctly close', async () => { +// const t = await create() - t.connect(defaultProductionDc.main, false) - await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) +// t.connect(defaultProductionDc.main, false) +// await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - t.close() +// t.close() - const socket = getLastSocket() +// const socket = getLastSocket() - expect(socket.removeAllListeners).toHaveBeenCalledOnce() - expect(socket.destroy).toHaveBeenCalledOnce() - }) +// expect(socket.removeAllListeners).toHaveBeenCalledOnce() +// expect(socket.destroy).toHaveBeenCalledOnce() +// }) - it('should feed data to the packet codec', async () => { - const t = await create() - const codec = t._packetCodec +// it('should feed data to the packet codec', async () => { +// const t = await create() +// const codec = t._packetCodec - const spyFeed = vi.spyOn(codec, 'feed') +// const spyFeed = vi.spyOn(codec, 'feed') - t.connect(defaultProductionDc.main, false) - await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) +// t.connect(defaultProductionDc.main, false) +// await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - const socket = getLastSocket() +// const socket = getLastSocket() - const onDataCall = socket.on.mock.calls.find(c => (c as string[])[0] === 'data') as unknown as [ - string, - (data: Uint8Array) => void, - ] - onDataCall[1](u8HexDecode('00010203040506070809')) +// const onDataCall = socket.on.mock.calls.find(c => (c as string[])[0] === 'data') as unknown as [ +// string, +// (data: Uint8Array) => void, +// ] +// onDataCall[1](u8HexDecode('00010203040506070809')) - expect(spyFeed).toHaveBeenCalledWith(u8HexDecode('00010203040506070809')) - }) +// expect(spyFeed).toHaveBeenCalledWith(u8HexDecode('00010203040506070809')) +// }) - it('should propagate errors', async () => { - const t = await create() +// it('should propagate errors', async () => { +// const t = await create() - const spyEmit = vi.fn() - t.on('error', spyEmit) +// const spyEmit = vi.fn() +// t.on('error', spyEmit) - t.connect(defaultProductionDc.main, false) - await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) +// t.connect(defaultProductionDc.main, false) +// await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - const socket = getLastSocket() +// const socket = getLastSocket() - const onErrorCall = socket.on.mock.calls.find(c => (c as string[])[0] === 'error') as unknown as [ - string, - (error: Error) => void, - ] - onErrorCall[1](new Error('test error')) +// const onErrorCall = socket.on.mock.calls.find(c => (c as string[])[0] === 'error') as unknown as [ +// string, +// (error: Error) => void, +// ] +// onErrorCall[1](new Error('test error')) - expect(spyEmit).toHaveBeenCalledWith(new Error('test error')) - }) - }) -} else { - describe.skip('TcpTransport', () => {}) -} +// expect(spyEmit).toHaveBeenCalledWith(new Error('test error')) +// }) +// }) +// } else { +// describe.skip('TcpTransport', () => {}) +// } diff --git a/packages/node/src/utils/tcp.ts b/packages/node/src/utils/tcp.ts index 8dafe64b..535d6502 100644 --- a/packages/node/src/utils/tcp.ts +++ b/packages/node/src/utils/tcp.ts @@ -1,140 +1,8 @@ -import EventEmitter from 'node:events' -import type { Socket } from 'node:net' -import { connect } from 'node:net' +import { connectTcp } from '@fuman/node-net' +import type { TelegramTransport } from '@mtcute/core' +import { IntermediatePacketCodec } from '@mtcute/core' -import type { IPacketCodec, ITelegramTransport } from '@mtcute/core' -import { IntermediatePacketCodec, MtcuteError, TransportState } from '@mtcute/core' -import type { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js' - -/** - * Base for TCP transports. - * Subclasses must provide packet codec in `_packetCodec` property - */ -export abstract class BaseTcpTransport extends EventEmitter implements ITelegramTransport { - protected _currentDc: BasicDcOption | null = null - protected _state: TransportState = TransportState.Idle - protected _socket: Socket | null = null - - abstract _packetCodec: IPacketCodec - protected _crypto!: ICryptoProvider - protected log!: Logger - - packetCodecInitialized = false - - private _updateLogPrefix() { - if (this._currentDc) { - this.log.prefix = `[TCP:${this._currentDc.ipAddress}:${this._currentDc.port}] ` - } else { - this.log.prefix = '[TCP:disconnected] ' - } - } - - setup(crypto: ICryptoProvider, log: Logger): void { - this._crypto = crypto - this.log = log.create('tcp') - this._updateLogPrefix() - } - - state(): TransportState { - return this._state - } - - currentDc(): BasicDcOption | null { - return this._currentDc - } - - // eslint-disable-next-line unused-imports/no-unused-vars - connect(dc: BasicDcOption, testMode: boolean): void { - if (this._state !== TransportState.Idle) { - throw new MtcuteError('Transport is not IDLE') - } - - if (!this.packetCodecInitialized) { - this._packetCodec.setup?.(this._crypto, this.log) - this._packetCodec.on('error', err => this.emit('error', err)) - this._packetCodec.on('packet', buf => this.emit('message', buf)) - this.packetCodecInitialized = true - } - - this._state = TransportState.Connecting - this._currentDc = dc - this._updateLogPrefix() - - this.log.debug('connecting to %j', dc) - - this._socket = connect(dc.port, dc.ipAddress, this.handleConnect.bind(this)) - - this._socket.on('data', (data) => { - this._packetCodec.feed(data) - }) - this._socket.on('error', this.handleError.bind(this)) - this._socket.on('close', this.close.bind(this)) - } - - close(): void { - if (this._state === TransportState.Idle) return - this.log.info('connection closed') - - this._state = TransportState.Idle - this._socket!.removeAllListeners() - this._socket!.destroy() - this._socket = null - this._currentDc = null - this._packetCodec.reset() - this.emit('close') - } - - handleError(error: Error): void { - this.log.error('error: %s', error.stack) - - if (this.listenerCount('error') > 0) { - this.emit('error', error) - } - } - - handleConnect(): void { - this.log.info('connected') - - Promise.resolve(this._packetCodec.tag()) - .then((initialMessage) => { - if (initialMessage.length) { - this._socket!.write(initialMessage, (err) => { - if (err) { - this.log.error('failed to write initial message: %s', err.stack) - this.emit('error') - this.close() - } else { - this._state = TransportState.Ready - this.emit('ready') - } - }) - } else { - this._state = TransportState.Ready - this.emit('ready') - } - }) - .catch(err => this.emit('error', err)) - } - - async send(bytes: Uint8Array): Promise { - const framed = await this._packetCodec.encode(bytes) - - if (this._state !== TransportState.Ready) { - throw new MtcuteError('Transport is not READY') - } - - return new Promise((resolve, reject) => { - this._socket!.write(framed, (error) => { - if (error) { - reject(error) - } else { - resolve() - } - }) - }) - } -} - -export class TcpTransport extends BaseTcpTransport { - _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec() +export const TcpTransport: TelegramTransport = { + connect: dc => connectTcp({ address: dc.ipAddress, port: dc.port }), + packetCodec: () => new IntermediatePacketCodec(), } diff --git a/packages/socks-proxy/index.ts b/packages/socks-proxy/index.ts index 67563e15..5cc286e1 100644 --- a/packages/socks-proxy/index.ts +++ b/packages/socks-proxy/index.ts @@ -1,419 +1,420 @@ -import { connect } from 'node:net' - -// @ts-expect-error no typings -import { normalize } from 'ip6' -import type { tl } from '@mtcute/node' -import { BaseTcpTransport, IntermediatePacketCodec, MtArgumentError, NodePlatform, TransportState, assertNever } from '@mtcute/node' -import { dataViewFromBuffer } from '@mtcute/node/utils.js' - -const p = new NodePlatform() - -/** - * An error has occurred while connecting to an SOCKS proxy - */ -export class SocksProxyConnectionError extends Error { - readonly proxy: SocksProxySettings - - constructor(proxy: SocksProxySettings, message: string) { - super(`Error while connecting to ${proxy.host}:${proxy.port}: ${message}`) - this.proxy = proxy - } -} - -/** - * Settings for a SOCKS4/5 proxy - */ -export interface SocksProxySettings { - /** - * Host or IP of the proxy (e.g. `proxy.example.com`, `1.2.3.4`) - */ - host: string - - /** - * Port of the proxy (e.g. `8888`) - */ - port: number - - /** - * Proxy authorization username, if needed - */ - user?: string - - /** - * Proxy authorization password, if needed - */ - password?: string - - /** - * Version of the SOCKS proxy (4 or 5) - * - * @default `5` - */ - version?: 4 | 5 -} - -function writeIpv4(ip: string, buf: Uint8Array, offset: number): void { - const parts = ip.split('.') - - if (parts.length !== 4) { - throw new MtArgumentError('Invalid IPv4 address') - } - for (let i = 0; i < 4; i++) { - const n = Number.parseInt(parts[i]) - - if (Number.isNaN(n) || n < 0 || n > 255) { - throw new MtArgumentError('Invalid IPv4 address') - } - - buf[offset + i] = n - } -} - -function buildSocks4ConnectRequest(ip: string, port: number, username = ''): Uint8Array { - const userId = p.utf8Encode(username) - const buf = new Uint8Array(9 + userId.length) - - buf[0] = 0x04 // VER - buf[1] = 0x01 // CMD = establish a TCP/IP stream connection - dataViewFromBuffer(buf).setUint16(2, port, false) - writeIpv4(ip, buf, 4) // DSTIP - buf.set(userId, 8) - buf[8 + userId.length] = 0x00 // ID (null-termination) - - return buf -} - -function buildSocks5Greeting(authAvailable: boolean): Uint8Array { - const buf = new Uint8Array(authAvailable ? 4 : 3) - - buf[0] = 0x05 // VER - - if (authAvailable) { - buf[1] = 0x02 // NAUTH - buf[2] = 0x00 // AUTH[0] = No authentication - buf[3] = 0x02 // AUTH[1] = Username/password - } else { - buf[1] = 0x01 // NAUTH - buf[2] = 0x00 // AUTH[0] = No authentication - } - - return buf -} - -function buildSocks5Auth(username: string, password: string) { - const usernameBuf = p.utf8Encode(username) - const passwordBuf = p.utf8Encode(password) - - if (usernameBuf.length > 255) { - throw new MtArgumentError(`Too long username (${usernameBuf.length} > 255)`) - } - if (passwordBuf.length > 255) { - throw new MtArgumentError(`Too long password (${passwordBuf.length} > 255)`) - } - - const buf = new Uint8Array(3 + usernameBuf.length + passwordBuf.length) - buf[0] = 0x01 // VER of auth - buf[1] = usernameBuf.length - buf.set(usernameBuf, 2) - buf[2 + usernameBuf.length] = passwordBuf.length - buf.set(passwordBuf, 3 + usernameBuf.length) - - return buf -} - -function writeIpv6(ip: string, buf: Uint8Array, offset: number): void { - // eslint-disable-next-line ts/no-unsafe-call - ip = normalize(ip) as string - const parts = ip.split(':') - - if (parts.length !== 8) { - throw new MtArgumentError('Invalid IPv6 address') - } - - const dv = dataViewFromBuffer(buf) - - for (let i = 0, j = offset; i < 8; i++, j += 2) { - const n = Number.parseInt(parts[i]) - - if (Number.isNaN(n) || n < 0 || n > 0xFFFF) { - throw new MtArgumentError('Invalid IPv6 address') - } - - dv.setUint16(j, n, false) - } -} - -function buildSocks5Connect(ip: string, port: number, ipv6 = false): Uint8Array { - const buf = new Uint8Array(ipv6 ? 22 : 10) - const dv = dataViewFromBuffer(buf) - - buf[0] = 0x05 // VER - buf[1] = 0x01 // CMD = establish a TCP/IP stream connection - buf[2] = 0x00 // RSV - - if (ipv6) { - buf[3] = 0x04 // TYPE = IPv6 - writeIpv6(ip, buf, 4) // ADDR - dv.setUint16(20, port, false) - } else { - buf[3] = 0x01 // TYPE = IPv4 - writeIpv4(ip, buf, 4) // ADDR - dv.setUint16(8, port, false) - } - - return buf -} - -const SOCKS4_ERRORS: Record = { - 91: 'Request rejected or failed', - 92: 'Request failed because client is not running identd', - 93: "Request failed because client's identd could not confirm the user ID in the request", -} - -const SOCKS5_ERRORS: Record = { - 1: 'General failure', - 2: 'Connection not allowed by ruleset', - 3: 'Network unreachable', - 4: 'Host unreachable', - 5: 'Connection refused by destination host', - 6: 'TTL expired', - 7: 'Command not supported / protocol error', - 8: 'Address type not supported', -} - -/** - * TCP transport that connects via a SOCKS4/5 proxy. - */ -export abstract class BaseSocksTcpTransport extends BaseTcpTransport { - readonly _proxy: SocksProxySettings - - constructor(proxy: SocksProxySettings) { - super() - - if (proxy.version != null && proxy.version !== 4 && proxy.version !== 5) { - throw new SocksProxyConnectionError( - proxy, - `Invalid SOCKS version: ${proxy.version}`, - ) - } - - this._proxy = proxy - } - - connect(dc: tl.RawDcOption): void { - if (this._state !== TransportState.Idle) { - throw new MtArgumentError('Transport is not IDLE') - } - - if (!this.packetCodecInitialized) { - this._packetCodec.on('error', err => this.emit('error', err)) - this._packetCodec.on('packet', buf => this.emit('message', buf)) - this.packetCodecInitialized = true - } - - this._state = TransportState.Connecting - this._currentDc = dc - - this._socket = connect(this._proxy.port, this._proxy.host, this._onProxyConnected.bind(this)) - - this._socket.on('error', this.handleError.bind(this)) - this._socket.on('close', this.close.bind(this)) - } - - private _onProxyConnected() { - let packetHandler: (msg: Uint8Array) => void - - if (this._proxy.version === 4) { - packetHandler = (msg) => { - if (msg[0] !== 0x04) { - // VER, must be 4 - this._socket!.emit( - 'error', - new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`), - ) - - return - } - const code = msg[1] - - this.log.debug('[%s:%d] CONNECT returned code %d', this._proxy.host, this._proxy.port, code) - - if (code === 0x5A) { - this._socket!.off('data', packetHandler) - this._socket!.on('data', data => this._packetCodec.feed(data)) - this.handleConnect() - } else { - const msg - = code in SOCKS4_ERRORS ? SOCKS4_ERRORS[code] : `Unknown error code: 0x${code.toString(16)}` - this._socket!.emit('error', new SocksProxyConnectionError(this._proxy, msg)) - } - } - - this.log.debug('[%s:%d] connected to proxy, sending CONNECT', this._proxy.host, this._proxy.port) - - try { - this._socket!.write( - buildSocks4ConnectRequest(this._currentDc!.ipAddress, this._currentDc!.port, this._proxy.user), - ) - } catch (e) { - this._socket!.emit('error', e) - } - } else { - let state: 'greeting' | 'auth' | 'connect' = 'greeting' - - const sendConnect = () => { - this.log.debug('[%s:%d] sending CONNECT', this._proxy.host, this._proxy.port) - - try { - this._socket!.write( - buildSocks5Connect(this._currentDc!.ipAddress, this._currentDc!.port, this._currentDc!.ipv6), - ) - state = 'connect' - } catch (e) { - this._socket!.emit('error', e) - } - } - - packetHandler = (msg) => { - switch (state) { - case 'greeting': { - if (msg[0] !== 0x05) { - // VER, must be 5 - this._socket!.emit( - 'error', - new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`), - ) - - return - } - - const chosen = msg[1] - - this.log.debug( - '[%s:%d] GREETING returned auth method %d', - this._proxy.host, - this._proxy.port, - chosen, - ) - - switch (chosen) { - case 0x00: - // "No authentication" - sendConnect() - break - case 0x02: - // Username/password - if (!this._proxy.user || !this._proxy.password) { - // should not happen - this._socket!.emit( - 'error', - new SocksProxyConnectionError( - this._proxy, - 'Authentication is required, but not provided', - ), - ) - break - } - - try { - this._socket!.write(buildSocks5Auth(this._proxy.user, this._proxy.password)) - state = 'auth' - } catch (e) { - this._socket!.emit('error', e) - } - break - case 0xFF: - default: - // "no acceptable methods were offered" - this._socket!.emit( - 'error', - new SocksProxyConnectionError( - this._proxy, - 'Authentication is required, but not provided/supported', - ), - ) - break - } - - break - } - case 'auth': - if (msg[0] !== 0x01) { - // VER of auth, must be 1 - this._socket!.emit( - 'error', - new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`), - ) - - return - } - - this.log.debug('[%s:%d] AUTH returned code %d', this._proxy.host, this._proxy.port, msg[1]) - - if (msg[1] === 0x00) { - // success - sendConnect() - } else { - this._socket!.emit( - 'error', - new SocksProxyConnectionError(this._proxy, 'Authentication failure'), - ) - } - break - - case 'connect': { - if (msg[0] !== 0x05) { - // VER, must be 5 - this._socket!.emit( - 'error', - new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`), - ) - - return - } - - const code = msg[1] - - this.log.debug('[%s:%d] CONNECT returned code %d', this._proxy.host, this._proxy.port, code) - - if (code === 0x00) { - // Request granted - this._socket!.off('data', packetHandler) - this._socket!.on('data', data => this._packetCodec.feed(data)) - this.handleConnect() - } else { - const msg - = code in SOCKS5_ERRORS - ? SOCKS5_ERRORS[code] - : `Unknown error code: 0x${code.toString(16)}` - this._socket!.emit('error', new SocksProxyConnectionError(this._proxy, msg)) - } - break - } - default: - assertNever(state) - } - } - - this.log.debug('[%s:%d] connected to proxy, sending GREETING', this._proxy.host, this._proxy.port) - - try { - this._socket!.write(buildSocks5Greeting(Boolean(this._proxy.user && this._proxy.password))) - } catch (e) { - this._socket!.emit('error', e) - } - } - - this._socket!.on('data', packetHandler) - } -} - -/** - * Socks TCP transport using an intermediate packet codec. - * - * Should be the one passed as `transport` to `TelegramClient` constructor - * (unless you want to use a custom codec). - */ -export class SocksTcpTransport extends BaseSocksTcpTransport { - _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec() -} +// todo: move to fuman +// import { connect } from 'node:net' + +// // @ts-expect-error no typings +// import { normalize } from 'ip6' +// import type { tl } from '@mtcute/node' +// import { BaseTcpTransport, IntermediatePacketCodec, MtArgumentError, NodePlatform, TransportState, assertNever } from '@mtcute/node' +// import { dataViewFromBuffer } from '@mtcute/node/utils.js' + +// const p = new NodePlatform() + +// /** +// * An error has occurred while connecting to an SOCKS proxy +// */ +// export class SocksProxyConnectionError extends Error { +// readonly proxy: SocksProxySettings + +// constructor(proxy: SocksProxySettings, message: string) { +// super(`Error while connecting to ${proxy.host}:${proxy.port}: ${message}`) +// this.proxy = proxy +// } +// } + +// /** +// * Settings for a SOCKS4/5 proxy +// */ +// export interface SocksProxySettings { +// /** +// * Host or IP of the proxy (e.g. `proxy.example.com`, `1.2.3.4`) +// */ +// host: string + +// /** +// * Port of the proxy (e.g. `8888`) +// */ +// port: number + +// /** +// * Proxy authorization username, if needed +// */ +// user?: string + +// /** +// * Proxy authorization password, if needed +// */ +// password?: string + +// /** +// * Version of the SOCKS proxy (4 or 5) +// * +// * @default `5` +// */ +// version?: 4 | 5 +// } + +// function writeIpv4(ip: string, buf: Uint8Array, offset: number): void { +// const parts = ip.split('.') + +// if (parts.length !== 4) { +// throw new MtArgumentError('Invalid IPv4 address') +// } +// for (let i = 0; i < 4; i++) { +// const n = Number.parseInt(parts[i]) + +// if (Number.isNaN(n) || n < 0 || n > 255) { +// throw new MtArgumentError('Invalid IPv4 address') +// } + +// buf[offset + i] = n +// } +// } + +// function buildSocks4ConnectRequest(ip: string, port: number, username = ''): Uint8Array { +// const userId = p.utf8Encode(username) +// const buf = new Uint8Array(9 + userId.length) + +// buf[0] = 0x04 // VER +// buf[1] = 0x01 // CMD = establish a TCP/IP stream connection +// dataViewFromBuffer(buf).setUint16(2, port, false) +// writeIpv4(ip, buf, 4) // DSTIP +// buf.set(userId, 8) +// buf[8 + userId.length] = 0x00 // ID (null-termination) + +// return buf +// } + +// function buildSocks5Greeting(authAvailable: boolean): Uint8Array { +// const buf = new Uint8Array(authAvailable ? 4 : 3) + +// buf[0] = 0x05 // VER + +// if (authAvailable) { +// buf[1] = 0x02 // NAUTH +// buf[2] = 0x00 // AUTH[0] = No authentication +// buf[3] = 0x02 // AUTH[1] = Username/password +// } else { +// buf[1] = 0x01 // NAUTH +// buf[2] = 0x00 // AUTH[0] = No authentication +// } + +// return buf +// } + +// function buildSocks5Auth(username: string, password: string) { +// const usernameBuf = p.utf8Encode(username) +// const passwordBuf = p.utf8Encode(password) + +// if (usernameBuf.length > 255) { +// throw new MtArgumentError(`Too long username (${usernameBuf.length} > 255)`) +// } +// if (passwordBuf.length > 255) { +// throw new MtArgumentError(`Too long password (${passwordBuf.length} > 255)`) +// } + +// const buf = new Uint8Array(3 + usernameBuf.length + passwordBuf.length) +// buf[0] = 0x01 // VER of auth +// buf[1] = usernameBuf.length +// buf.set(usernameBuf, 2) +// buf[2 + usernameBuf.length] = passwordBuf.length +// buf.set(passwordBuf, 3 + usernameBuf.length) + +// return buf +// } + +// function writeIpv6(ip: string, buf: Uint8Array, offset: number): void { +// // eslint-disable-next-line ts/no-unsafe-call +// ip = normalize(ip) as string +// const parts = ip.split(':') + +// if (parts.length !== 8) { +// throw new MtArgumentError('Invalid IPv6 address') +// } + +// const dv = dataViewFromBuffer(buf) + +// for (let i = 0, j = offset; i < 8; i++, j += 2) { +// const n = Number.parseInt(parts[i]) + +// if (Number.isNaN(n) || n < 0 || n > 0xFFFF) { +// throw new MtArgumentError('Invalid IPv6 address') +// } + +// dv.setUint16(j, n, false) +// } +// } + +// function buildSocks5Connect(ip: string, port: number, ipv6 = false): Uint8Array { +// const buf = new Uint8Array(ipv6 ? 22 : 10) +// const dv = dataViewFromBuffer(buf) + +// buf[0] = 0x05 // VER +// buf[1] = 0x01 // CMD = establish a TCP/IP stream connection +// buf[2] = 0x00 // RSV + +// if (ipv6) { +// buf[3] = 0x04 // TYPE = IPv6 +// writeIpv6(ip, buf, 4) // ADDR +// dv.setUint16(20, port, false) +// } else { +// buf[3] = 0x01 // TYPE = IPv4 +// writeIpv4(ip, buf, 4) // ADDR +// dv.setUint16(8, port, false) +// } + +// return buf +// } + +// const SOCKS4_ERRORS: Record = { +// 91: 'Request rejected or failed', +// 92: 'Request failed because client is not running identd', +// 93: "Request failed because client's identd could not confirm the user ID in the request", +// } + +// const SOCKS5_ERRORS: Record = { +// 1: 'General failure', +// 2: 'Connection not allowed by ruleset', +// 3: 'Network unreachable', +// 4: 'Host unreachable', +// 5: 'Connection refused by destination host', +// 6: 'TTL expired', +// 7: 'Command not supported / protocol error', +// 8: 'Address type not supported', +// } + +// /** +// * TCP transport that connects via a SOCKS4/5 proxy. +// */ +// export abstract class BaseSocksTcpTransport extends BaseTcpTransport { +// readonly _proxy: SocksProxySettings + +// constructor(proxy: SocksProxySettings) { +// super() + +// if (proxy.version != null && proxy.version !== 4 && proxy.version !== 5) { +// throw new SocksProxyConnectionError( +// proxy, +// `Invalid SOCKS version: ${proxy.version}`, +// ) +// } + +// this._proxy = proxy +// } + +// connect(dc: tl.RawDcOption): void { +// if (this._state !== TransportState.Idle) { +// throw new MtArgumentError('Transport is not IDLE') +// } + +// if (!this.packetCodecInitialized) { +// this._packetCodec.on('error', err => this.emit('error', err)) +// this._packetCodec.on('packet', buf => this.emit('message', buf)) +// this.packetCodecInitialized = true +// } + +// this._state = TransportState.Connecting +// this._currentDc = dc + +// this._socket = connect(this._proxy.port, this._proxy.host, this._onProxyConnected.bind(this)) + +// this._socket.on('error', this.handleError.bind(this)) +// this._socket.on('close', this.close.bind(this)) +// } + +// private _onProxyConnected() { +// let packetHandler: (msg: Uint8Array) => void + +// if (this._proxy.version === 4) { +// packetHandler = (msg) => { +// if (msg[0] !== 0x04) { +// // VER, must be 4 +// this._socket!.emit( +// 'error', +// new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`), +// ) + +// return +// } +// const code = msg[1] + +// this.log.debug('[%s:%d] CONNECT returned code %d', this._proxy.host, this._proxy.port, code) + +// if (code === 0x5A) { +// this._socket!.off('data', packetHandler) +// this._socket!.on('data', data => this._packetCodec.feed(data)) +// this.handleConnect() +// } else { +// const msg +// = code in SOCKS4_ERRORS ? SOCKS4_ERRORS[code] : `Unknown error code: 0x${code.toString(16)}` +// this._socket!.emit('error', new SocksProxyConnectionError(this._proxy, msg)) +// } +// } + +// this.log.debug('[%s:%d] connected to proxy, sending CONNECT', this._proxy.host, this._proxy.port) + +// try { +// this._socket!.write( +// buildSocks4ConnectRequest(this._currentDc!.ipAddress, this._currentDc!.port, this._proxy.user), +// ) +// } catch (e) { +// this._socket!.emit('error', e) +// } +// } else { +// let state: 'greeting' | 'auth' | 'connect' = 'greeting' + +// const sendConnect = () => { +// this.log.debug('[%s:%d] sending CONNECT', this._proxy.host, this._proxy.port) + +// try { +// this._socket!.write( +// buildSocks5Connect(this._currentDc!.ipAddress, this._currentDc!.port, this._currentDc!.ipv6), +// ) +// state = 'connect' +// } catch (e) { +// this._socket!.emit('error', e) +// } +// } + +// packetHandler = (msg) => { +// switch (state) { +// case 'greeting': { +// if (msg[0] !== 0x05) { +// // VER, must be 5 +// this._socket!.emit( +// 'error', +// new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`), +// ) + +// return +// } + +// const chosen = msg[1] + +// this.log.debug( +// '[%s:%d] GREETING returned auth method %d', +// this._proxy.host, +// this._proxy.port, +// chosen, +// ) + +// switch (chosen) { +// case 0x00: +// // "No authentication" +// sendConnect() +// break +// case 0x02: +// // Username/password +// if (!this._proxy.user || !this._proxy.password) { +// // should not happen +// this._socket!.emit( +// 'error', +// new SocksProxyConnectionError( +// this._proxy, +// 'Authentication is required, but not provided', +// ), +// ) +// break +// } + +// try { +// this._socket!.write(buildSocks5Auth(this._proxy.user, this._proxy.password)) +// state = 'auth' +// } catch (e) { +// this._socket!.emit('error', e) +// } +// break +// case 0xFF: +// default: +// // "no acceptable methods were offered" +// this._socket!.emit( +// 'error', +// new SocksProxyConnectionError( +// this._proxy, +// 'Authentication is required, but not provided/supported', +// ), +// ) +// break +// } + +// break +// } +// case 'auth': +// if (msg[0] !== 0x01) { +// // VER of auth, must be 1 +// this._socket!.emit( +// 'error', +// new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`), +// ) + +// return +// } + +// this.log.debug('[%s:%d] AUTH returned code %d', this._proxy.host, this._proxy.port, msg[1]) + +// if (msg[1] === 0x00) { +// // success +// sendConnect() +// } else { +// this._socket!.emit( +// 'error', +// new SocksProxyConnectionError(this._proxy, 'Authentication failure'), +// ) +// } +// break + +// case 'connect': { +// if (msg[0] !== 0x05) { +// // VER, must be 5 +// this._socket!.emit( +// 'error', +// new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`), +// ) + +// return +// } + +// const code = msg[1] + +// this.log.debug('[%s:%d] CONNECT returned code %d', this._proxy.host, this._proxy.port, code) + +// if (code === 0x00) { +// // Request granted +// this._socket!.off('data', packetHandler) +// this._socket!.on('data', data => this._packetCodec.feed(data)) +// this.handleConnect() +// } else { +// const msg +// = code in SOCKS5_ERRORS +// ? SOCKS5_ERRORS[code] +// : `Unknown error code: 0x${code.toString(16)}` +// this._socket!.emit('error', new SocksProxyConnectionError(this._proxy, msg)) +// } +// break +// } +// default: +// assertNever(state) +// } +// } + +// this.log.debug('[%s:%d] connected to proxy, sending GREETING', this._proxy.host, this._proxy.port) + +// try { +// this._socket!.write(buildSocks5Greeting(Boolean(this._proxy.user && this._proxy.password))) +// } catch (e) { +// this._socket!.emit('error', e) +// } +// } + +// this._socket!.on('data', packetHandler) +// } +// } + +// /** +// * Socks TCP transport using an intermediate packet codec. +// * +// * Should be the one passed as `transport` to `TelegramClient` constructor +// * (unless you want to use a custom codec). +// */ +// export class SocksTcpTransport extends BaseSocksTcpTransport { +// _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec() +// } diff --git a/packages/test/src/client.ts b/packages/test/src/client.ts index 5be43e23..cc600ed8 100644 --- a/packages/test/src/client.ts +++ b/packages/test/src/client.ts @@ -1,11 +1,11 @@ import type { MaybePromise, MustEqual, RpcCallOptions } from '@mtcute/core' -import { tl } from '@mtcute/core' +import { IntermediatePacketCodec, tl } from '@mtcute/core' import type { BaseTelegramClientOptions } from '@mtcute/core/client.js' import { BaseTelegramClient } from '@mtcute/core/client.js' import { defaultCryptoProvider } from './platform.js' import { StubMemoryTelegramStorage } from './storage.js' -import { StubTelegramTransport } from './transport.js' +// import { StubTelegramTransport } from './transport.js' import type { InputResponder } from './types.js' import { markedIdToPeer } from './utils.js' @@ -29,27 +29,32 @@ export class StubTelegramClient extends BaseTelegramClient { logLevel: 5, storage, disableUpdates: true, - transport: () => { - const transport = new StubTelegramTransport({ - onMessage: (data) => { - if (!this._onRawMessage) { - if (this._responders.size) { - this.emitError(new Error('Unexpected outgoing message')) - } + transport: { + connect: () => { + // const transport = new StubTelegramTransport({ + // onMessage: (data) => { + // if (!this._onRawMessage) { + // if (this._responders.size) { + // this.emitError(new Error('Unexpected outgoing message')) + // } - return - } + // return + // } - const dcId = transport._currentDc!.id - const key = storage.authKeys.get(dcId) + // const dcId = transport._currentDc!.id + // const key = storage.authKeys.get(dcId) - if (key) { - this._onRawMessage(storage.decryptOutgoingMessage(transport._crypto, data, dcId)) - } - }, - }) + // if (key) { + // this._onRawMessage(storage.decryptOutgoingMessage(transport._crypto, data, dcId)) + // } + // }, + // }) - return transport + // return transport + // todo: fuman + throw new Error('not implemented') + }, + packetCodec: () => new IntermediatePacketCodec(), }, crypto: defaultCryptoProvider, ...params, diff --git a/packages/test/src/index.ts b/packages/test/src/index.ts index b3755c23..8f2e4bf3 100644 --- a/packages/test/src/index.ts +++ b/packages/test/src/index.ts @@ -4,5 +4,4 @@ export * from './platform.js' export * from './storage.js' export * from './storage/index.js' export * from './stub.js' -export * from './transport.js' export * from './types.js' diff --git a/packages/test/src/transport.test.ts b/packages/test/src/transport.test.ts index abba0492..de009265 100644 --- a/packages/test/src/transport.test.ts +++ b/packages/test/src/transport.test.ts @@ -1,44 +1,45 @@ -import { describe, expect, it, vi } from 'vitest' -import { MemoryStorage } from '@mtcute/core' -import { BaseTelegramClient } from '@mtcute/core/client.js' +// todo: fuman +// import { describe, expect, it, vi } from 'vitest' +// import { MemoryStorage } from '@mtcute/core' +// import { BaseTelegramClient } from '@mtcute/core/client.js' -import { defaultCryptoProvider } from './platform.js' -import { createStub } from './stub.js' -import { StubTelegramTransport } from './transport.js' +// import { defaultCryptoProvider } from './platform.js' +// import { createStub } from './stub.js' +// import { StubTelegramTransport } from './transport.js' -describe('transport stub', () => { - it('should correctly intercept calls', async () => { - const log: string[] = [] +// describe('transport stub', () => { +// it('should correctly intercept calls', async () => { +// const log: string[] = [] - const client = new BaseTelegramClient({ - apiId: 0, - apiHash: '', - logLevel: 0, - defaultDcs: { - main: createStub('dcOption', { ipAddress: '1.2.3.4', port: 1234 }), - media: createStub('dcOption', { ipAddress: '1.2.3.4', port: 5678 }), - }, - storage: new MemoryStorage(), - crypto: defaultCryptoProvider, - transport: () => - new StubTelegramTransport({ - onConnect: (dc, testMode) => { - log.push(`connect ${dc.ipAddress}:${dc.port} test=${testMode}`) - client.close().catch(() => {}) - }, - onMessage(msg) { - log.push(`message size=${msg.length}`) - }, - }), - }) +// const client = new BaseTelegramClient({ +// apiId: 0, +// apiHash: '', +// logLevel: 0, +// defaultDcs: { +// main: createStub('dcOption', { ipAddress: '1.2.3.4', port: 1234 }), +// media: createStub('dcOption', { ipAddress: '1.2.3.4', port: 5678 }), +// }, +// storage: new MemoryStorage(), +// crypto: defaultCryptoProvider, +// transport: () => +// new StubTelegramTransport({ +// onConnect: (dc, testMode) => { +// log.push(`connect ${dc.ipAddress}:${dc.port} test=${testMode}`) +// client.close().catch(() => {}) +// }, +// onMessage(msg) { +// log.push(`message size=${msg.length}`) +// }, +// }), +// }) - client.connect().catch(() => {}) // ignore "client closed" error +// client.connect().catch(() => {}) // ignore "client closed" error - await vi.waitFor(() => - expect(log).toEqual([ - 'message size=40', // req_pq_multi - 'connect 1.2.3.4:1234 test=false', - ]), - ) - }) -}) +// await vi.waitFor(() => +// expect(log).toEqual([ +// 'message size=40', // req_pq_multi +// 'connect 1.2.3.4:1234 test=false', +// ]), +// ) +// }) +// }) diff --git a/packages/test/src/transport.ts b/packages/test/src/transport.ts index 8175da27..720f47b2 100644 --- a/packages/test/src/transport.ts +++ b/packages/test/src/transport.ts @@ -1,68 +1,68 @@ -// eslint-disable-next-line unicorn/prefer-node-protocol -import EventEmitter from 'events' +// todo: implement in fuman +// import EventEmitter from 'node:events' -import type { ITelegramTransport } from '@mtcute/core' -import { TransportState } from '@mtcute/core' -import type { ICryptoProvider, Logger } from '@mtcute/core/utils.js' -import type { tl } from '@mtcute/tl' +// import type { ITelegramTransport } from '@mtcute/core' +// import { TransportState } from '@mtcute/core' +// import type { ICryptoProvider, Logger } from '@mtcute/core/utils.js' +// import type { tl } from '@mtcute/tl' -export class StubTelegramTransport extends EventEmitter implements ITelegramTransport { - constructor( - readonly params: { - getMtproxyInfo?: () => tl.RawInputClientProxy - onConnect?: (dc: tl.RawDcOption, testMode: boolean) => void - onClose?: () => void - onMessage?: (msg: Uint8Array) => void - }, - ) { - super() +// export class StubTelegramTransport extends EventEmitter implements ITelegramConnection { +// constructor( +// readonly params: { +// getMtproxyInfo?: () => tl.RawInputClientProxy +// onConnect?: (dc: tl.RawDcOption, testMode: boolean) => void +// onClose?: () => void +// onMessage?: (msg: Uint8Array) => void +// }, +// ) { +// super() - if (params.getMtproxyInfo) { - (this as unknown as ITelegramTransport).getMtproxyInfo = params.getMtproxyInfo - } - } +// if (params.getMtproxyInfo) { +// (this as unknown as ITelegramTransport).getMtproxyInfo = params.getMtproxyInfo +// } +// } - _state: TransportState = TransportState.Idle - _currentDc: tl.RawDcOption | null = null - _crypto!: ICryptoProvider - _log!: Logger +// _state: TransportState = TransportState.Idle +// _currentDc: tl.RawDcOption | null = null +// _crypto!: ICryptoProvider +// _log!: Logger - write(data: Uint8Array): void { - this.emit('message', data) - } +// write(data: Uint8Array): void { +// this.emit('message', data) +// } - setup(crypto: ICryptoProvider, log: Logger): void { - this._crypto = crypto - this._log = log - } +// setup(crypto: ICryptoProvider, log: Logger): void { +// this._crypto = crypto +// this._log = log +// } - state(): TransportState { - return this._state - } +// state(): TransportState { +// return this._state +// } - currentDc(): tl.RawDcOption | null { - return this._currentDc - } +// currentDc(): tl.RawDcOption | null { +// return this._currentDc +// } - connect(dc: tl.RawDcOption, testMode: boolean): void { - this._currentDc = dc - this._state = TransportState.Ready - this.emit('ready') - this._log.debug('stubbing connection to %s:%d', dc.ipAddress, dc.port) +// connect(dc: tl.RawDcOption, testMode: boolean): void { +// this._currentDc = dc +// this._state = TransportState.Ready +// this.emit('ready') +// this._log.debug('stubbing connection to %s:%d', dc.ipAddress, dc.port) - this.params.onConnect?.(dc, testMode) - } +// this.params.onConnect?.(dc, testMode) +// } - close(): void { - this._currentDc = null - this._state = TransportState.Idle - this.emit('close') - this._log.debug('stub connection closed') +// close(): void { +// this._currentDc = null +// this._state = TransportState.Idle +// this.emit('close') +// this._log.debug('stub connection closed') - this.params.onClose?.() - } +// this.params.onClose?.() +// } - async send(data: Uint8Array): Promise { - this.params.onMessage?.(data) - } -} +// async send(data: Uint8Array): Promise { +// this.params.onMessage?.(data) +// } +// } diff --git a/packages/web/package.json b/packages/web/package.json index 7c9ef6ec..2edb218f 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,36 +1,37 @@ { - "name": "@mtcute/web", - "type": "module", - "version": "0.16.13", - "private": true, - "description": "Meta-package for the web platform", - "author": "alina sireneva ", - "license": "MIT", - "sideEffects": false, - "exports": { - ".": "./src/index.ts", - "./utils.js": "./src/utils.ts", - "./methods.js": "./src/methods.ts" - }, - "scripts": { - "docs": "typedoc", - "build": "pnpm run -w build-package web" - }, - "dependencies": { - "@mtcute/core": "workspace:^", - "@mtcute/wasm": "workspace:^", - "events": "3.2.0" - }, - "devDependencies": { - "@mtcute/test": "workspace:^" - }, - "denoJson": { - "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "WebWorker" - ] + "name": "@mtcute/web", + "type": "module", + "version": "0.16.13", + "private": true, + "description": "Meta-package for the web platform", + "author": "alina sireneva ", + "license": "MIT", + "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./utils.js": "./src/utils.ts", + "./methods.js": "./src/methods.ts" + }, + "scripts": { + "docs": "typedoc", + "build": "pnpm run -w build-package web" + }, + "dependencies": { + "@mtcute/core": "workspace:^", + "@mtcute/wasm": "workspace:^", + "@fuman/net": "workspace:^", + "events": "3.2.0" + }, + "devDependencies": { + "@mtcute/test": "workspace:^" + }, + "denoJson": { + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "WebWorker" + ] + } } - } } diff --git a/packages/web/src/client.ts b/packages/web/src/client.ts index 0b854b34..9e35b1ab 100644 --- a/packages/web/src/client.ts +++ b/packages/web/src/client.ts @@ -43,7 +43,7 @@ export class BaseTelegramClient extends BaseTelegramClientBase { super({ crypto: new WebCryptoProvider(), - transport: () => new WebSocketTransport(), + transport: new WebSocketTransport(), ...opts, storage: typeof opts.storage === 'string' diff --git a/packages/web/src/websocket.test.ts b/packages/web/src/websocket.test.ts index 0e40844a..03feab54 100644 --- a/packages/web/src/websocket.test.ts +++ b/packages/web/src/websocket.test.ts @@ -1,140 +1,141 @@ -import type { Mock, MockedObject } from 'vitest' -import { describe, expect, it, vi } from 'vitest' -import { TransportState } from '@mtcute/core' -import { getPlatform } from '@mtcute/core/platform.js' -import { LogManager, defaultProductionDc } from '@mtcute/core/utils.js' -import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test' +// todo: move to fuman +// import type { Mock, MockedObject } from 'vitest' +// import { describe, expect, it, vi } from 'vitest' +// import { TransportState } from '@mtcute/core' +// import { getPlatform } from '@mtcute/core/platform.js' +// import { LogManager, defaultProductionDc } from '@mtcute/core/utils.js' +// import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test' -import { WebSocketTransport } from './websocket.js' +// import { WebSocketTransport } from './websocket.js' -const p = getPlatform() +// const p = getPlatform() -describe('WebSocketTransport', () => { - const create = async () => { - let closeListener: () => void = () => {} - const fakeWs = vi.fn().mockImplementation(() => ({ - addEventListener: vi.fn().mockImplementation((event: string, cb: () => void) => { - if (event === 'open') { - cb() - } - if (event === 'close') { - closeListener = cb - } - }), - removeEventListener: vi.fn(), - close: vi.fn().mockImplementation(() => closeListener()), - send: vi.fn(), - })) +// describe('WebSocketTransport', () => { +// const create = async () => { +// let closeListener: () => void = () => {} +// const fakeWs = vi.fn().mockImplementation(() => ({ +// addEventListener: vi.fn().mockImplementation((event: string, cb: () => void) => { +// if (event === 'open') { +// cb() +// } +// if (event === 'close') { +// closeListener = cb +// } +// }), +// removeEventListener: vi.fn(), +// close: vi.fn().mockImplementation(() => closeListener()), +// send: vi.fn(), +// })) - const transport = new WebSocketTransport({ ws: fakeWs }) - const logger = new LogManager() - logger.level = 10 - transport.setup(await defaultTestCryptoProvider(), logger) +// const transport = new WebSocketTransport({ ws: fakeWs }) +// const logger = new LogManager() +// logger.level = 10 +// transport.setup(await defaultTestCryptoProvider(), logger) - return [transport, fakeWs] as const - } +// return [transport, fakeWs] as const +// } - const getLastSocket = (ws: Mock) => { - return ws.mock.results[ws.mock.results.length - 1].value as MockedObject - } +// const getLastSocket = (ws: Mock) => { +// return ws.mock.results[ws.mock.results.length - 1].value as MockedObject +// } - it('should initiate a websocket connection to the given dc', async () => { - const [t, ws] = await create() +// it('should initiate a websocket connection to the given dc', async () => { +// const [t, ws] = await create() - t.connect(defaultProductionDc.main, false) +// t.connect(defaultProductionDc.main, false) - expect(ws).toHaveBeenCalledOnce() - expect(ws).toHaveBeenCalledWith('wss://venus.web.telegram.org/apiws', 'binary') - await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - }) +// expect(ws).toHaveBeenCalledOnce() +// expect(ws).toHaveBeenCalledWith('wss://venus.web.telegram.org/apiws', 'binary') +// await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) +// }) - it('should set up event handlers', async () => { - const [t, ws] = await create() +// it('should set up event handlers', async () => { +// const [t, ws] = await create() - t.connect(defaultProductionDc.main, false) - const socket = getLastSocket(ws) +// t.connect(defaultProductionDc.main, false) +// const socket = getLastSocket(ws) - expect(socket.addEventListener).toHaveBeenCalledWith('message', expect.any(Function)) - expect(socket.addEventListener).toHaveBeenCalledWith('error', expect.any(Function)) - expect(socket.addEventListener).toHaveBeenCalledWith('close', expect.any(Function)) - }) +// expect(socket.addEventListener).toHaveBeenCalledWith('message', expect.any(Function)) +// expect(socket.addEventListener).toHaveBeenCalledWith('error', expect.any(Function)) +// expect(socket.addEventListener).toHaveBeenCalledWith('close', expect.any(Function)) +// }) - it('should write packet codec tag to the socket', async () => { - const [t, ws] = await create() +// it('should write packet codec tag to the socket', async () => { +// const [t, ws] = await create() - t.connect(defaultProductionDc.main, false) - const socket = getLastSocket(ws) +// t.connect(defaultProductionDc.main, false) +// const socket = getLastSocket(ws) - await vi.waitFor(() => - expect(socket.send).toHaveBeenCalledWith( - u8HexDecode( - '29afd26df40fb8ed10b6b4ad6d56ef5df9453f88e6ee6adb6e0544ba635dc6a8a990c9b8b980c343936b33fa7f97bae025102532233abb26d4a1fe6d34f1ba08', - ), - ), - ) - }) +// await vi.waitFor(() => +// expect(socket.send).toHaveBeenCalledWith( +// u8HexDecode( +// '29afd26df40fb8ed10b6b4ad6d56ef5df9453f88e6ee6adb6e0544ba635dc6a8a990c9b8b980c343936b33fa7f97bae025102532233abb26d4a1fe6d34f1ba08', +// ), +// ), +// ) +// }) - it('should write to the underlying socket', async () => { - const [t, ws] = await create() +// it('should write to the underlying socket', async () => { +// const [t, ws] = await create() - t.connect(defaultProductionDc.main, false) - const socket = getLastSocket(ws) - await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) +// t.connect(defaultProductionDc.main, false) +// const socket = getLastSocket(ws) +// await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - await t.send(p.hexDecode('00010203040506070809')) +// await t.send(p.hexDecode('00010203040506070809')) - expect(socket.send).toHaveBeenCalledWith(u8HexDecode('af020630c8ef14bcf53af33853ea')) - }) +// expect(socket.send).toHaveBeenCalledWith(u8HexDecode('af020630c8ef14bcf53af33853ea')) +// }) - it('should correctly close', async () => { - const [t, ws] = await create() +// it('should correctly close', async () => { +// const [t, ws] = await create() - t.connect(defaultProductionDc.main, false) - const socket = getLastSocket(ws) - await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) +// t.connect(defaultProductionDc.main, false) +// const socket = getLastSocket(ws) +// await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - await t.close() +// await t.close() - expect(socket.close).toHaveBeenCalled() - }) +// expect(socket.close).toHaveBeenCalled() +// }) - it('should correctly handle incoming messages', async () => { - const [t, ws] = await create() +// it('should correctly handle incoming messages', async () => { +// const [t, ws] = await create() - const feedSpy = vi.spyOn(t._packetCodec, 'feed') +// const feedSpy = vi.spyOn(t._packetCodec, 'feed') - t.connect(defaultProductionDc.main, false) - const socket = getLastSocket(ws) - await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) +// t.connect(defaultProductionDc.main, false) +// const socket = getLastSocket(ws) +// await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - const data = p.hexDecode('00010203040506070809') - const message = new MessageEvent('message', { data }) +// const data = p.hexDecode('00010203040506070809') +// const message = new MessageEvent('message', { data }) - const onMessageCall = socket.addEventListener.mock.calls.find(([event]) => event === 'message') as unknown as [ - string, - (evt: MessageEvent) => void, - ] - onMessageCall[1](message) +// const onMessageCall = socket.addEventListener.mock.calls.find(([event]) => event === 'message') as unknown as [ +// string, +// (evt: MessageEvent) => void, +// ] +// onMessageCall[1](message) - expect(feedSpy).toHaveBeenCalledWith(u8HexDecode('00010203040506070809')) - }) +// expect(feedSpy).toHaveBeenCalledWith(u8HexDecode('00010203040506070809')) +// }) - it('should propagate errors', async () => { - const [t, ws] = await create() +// it('should propagate errors', async () => { +// const [t, ws] = await create() - const spyEmit = vi.spyOn(t, 'emit').mockImplementation(() => true) +// const spyEmit = vi.spyOn(t, 'emit').mockImplementation(() => true) - t.connect(defaultProductionDc.main, false) - const socket = getLastSocket(ws) - await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) +// t.connect(defaultProductionDc.main, false) +// const socket = getLastSocket(ws) +// await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - const error = new Error('test') - const onErrorCall = socket.addEventListener.mock.calls.find(([event]) => event === 'error') as unknown as [ - string, - (evt: { error: Error }) => void, - ] - onErrorCall[1]({ error }) +// const error = new Error('test') +// const onErrorCall = socket.addEventListener.mock.calls.find(([event]) => event === 'error') as unknown as [ +// string, +// (evt: { error: Error }) => void, +// ] +// onErrorCall[1]({ error }) - expect(spyEmit).toHaveBeenCalledWith('error', error) - }) -}) +// expect(spyEmit).toHaveBeenCalledWith('error', error) +// }) +// }) diff --git a/packages/web/src/websocket.ts b/packages/web/src/websocket.ts index 5b6ce064..5b4d6eb0 100644 --- a/packages/web/src/websocket.ts +++ b/packages/web/src/websocket.ts @@ -1,26 +1,16 @@ -// eslint-disable-next-line unicorn/prefer-node-protocol -import EventEmitter from 'events' - import type { IPacketCodec, - ITelegramTransport, + ITelegramConnection, + TelegramTransport, } from '@mtcute/core' import { IntermediatePacketCodec, MtUnsupportedError, - MtcuteError, ObfuscatedPacketCodec, - TransportState, } from '@mtcute/core' -import type { - BasicDcOption, - ControllablePromise, - ICryptoProvider, - Logger, -} from '@mtcute/core/utils.js' -import { - createControllablePromise, -} from '@mtcute/core/utils.js' +import { WebSocketConnection } from '@fuman/net' + +import type { BasicDcOption } from './utils' export interface WebSocketConstructor { new (address: string, protocol?: string): WebSocket @@ -34,20 +24,7 @@ const subdomainsMap: Record = { 5: 'flora', } -/** - * Base for WebSocket transports. - * Subclasses must provide packet codec in `_packetCodec` property - */ -export abstract class BaseWebSocketTransport extends EventEmitter implements ITelegramTransport { - private _currentDc: BasicDcOption | null = null - private _state: TransportState = TransportState.Idle - private _socket: WebSocket | null = null - private _crypto!: ICryptoProvider - protected log!: Logger - - abstract _packetCodec: IPacketCodec - packetCodecInitialized = false - +export class WebSocketTransport implements TelegramTransport { private _baseDomain: string private _subdomains: Record private _WebSocket: WebSocketConstructor @@ -64,8 +41,6 @@ export abstract class BaseWebSocketTransport extends EventEmitter implements ITe /** Map of sub-domains (key is DC ID, value is string) */ subdomains?: Record } = {}) { - super() - if (!ws) { throw new MtUnsupportedError( 'To use WebSocket transport with NodeJS, install `ws` package and pass it to constructor', @@ -82,114 +57,25 @@ export abstract class BaseWebSocketTransport extends EventEmitter implements ITe this._WebSocket = ws } - private _updateLogPrefix() { - if (this._currentDc) { - this.log.prefix = `[WS:${this._subdomains[this._currentDc.id]}.${this._baseDomain}] ` - } else { - this.log.prefix = '[WS:disconnected] ' - } - } + async connect(dc: BasicDcOption, testMode: boolean): Promise { + const url = `wss://${this._subdomains[dc.id]}.${this._baseDomain}/apiws${testMode ? '_test' : ''}` - setup(crypto: ICryptoProvider, log: Logger): void { - this._crypto = crypto - this.log = log.create('ws') - } + return new Promise((resolve, reject) => { + const socket = new this._WebSocket(url) - state(): TransportState { - return this._state - } - - currentDc(): BasicDcOption | null { - return this._currentDc - } - - connect(dc: BasicDcOption, testMode: boolean): void { - if (this._state !== TransportState.Idle) { - throw new MtcuteError('Transport is not IDLE') - } - - if (!this.packetCodecInitialized) { - this._packetCodec.setup?.(this._crypto, this.log) - this._packetCodec.on('error', err => this.emit('error', err)) - this._packetCodec.on('packet', buf => this.emit('message', buf)) - this.packetCodecInitialized = true - } - - this._state = TransportState.Connecting - this._currentDc = dc - this._socket = new this._WebSocket( - `wss://${this._subdomains[dc.id]}.${this._baseDomain}/apiws${testMode ? '_test' : ''}`, - 'binary', - ) - - this._updateLogPrefix() - this.log.debug('connecting to %s (%j)', this._socket.url, dc) - - this._socket.binaryType = 'arraybuffer' - - this._socket.addEventListener('message', evt => - this._packetCodec.feed(new Uint8Array(evt.data as ArrayBuffer))) - this._socket.addEventListener('open', this.handleConnect.bind(this)) - this._socket.addEventListener('error', this.handleError.bind(this)) - this._socket.addEventListener('close', this.handleClosed.bind(this)) - } - - private _closeWaiters: ControllablePromise[] = [] - async close(): Promise { - if (this._state === TransportState.Idle) return - - const promise = createControllablePromise() - this._closeWaiters.push(promise) - - this._socket!.close() - - return promise - } - - handleClosed(): void { - this.log.info('connection closed') - this._state = TransportState.Idle - this._socket = null - this._currentDc = null - this._packetCodec.reset() - this.emit('close') - - for (const waiter of this._closeWaiters) { - waiter.resolve() - } - this._closeWaiters = [] - } - - handleError(event: Event | { error: Error }): void { - const error = 'error' in event ? event.error : new Error('unknown WebSocket error') - - this.log.error('error: %s', error.stack) - this.emit('error', error) - } - - handleConnect(): void { - this.log.info('connected') - - Promise.resolve(this._packetCodec.tag()) - .then((initialMessage) => { - this._socket!.send(initialMessage) - this._state = TransportState.Ready - this.emit('ready') + const onError = (event: Event) => { + socket.removeEventListener('error', onError) + reject(event) + } + socket.addEventListener('error', onError) + socket.addEventListener('open', () => { + socket.removeEventListener('error', onError) + resolve(new WebSocketConnection(socket)) }) - .catch(err => this.emit('error', err)) + }) } - async send(bytes: Uint8Array): Promise { - if (this._state !== TransportState.Ready) { - throw new MtcuteError('Transport is not READY') - } - - const framed = await this._packetCodec.encode(bytes) - - this._socket!.send(framed) + packetCodec(): IPacketCodec { + return new ObfuscatedPacketCodec(new IntermediatePacketCodec()) } } - -export class WebSocketTransport extends BaseWebSocketTransport { - _packetCodec: ObfuscatedPacketCodec = new ObfuscatedPacketCodec(new IntermediatePacketCodec()) -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0e8642f..4cc6eb76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,6 +130,15 @@ importers: packages/core: dependencies: + '@fuman/io': + specifier: workspace:^ + version: link:../../private/fuman/packages/io + '@fuman/net': + specifier: workspace:^ + version: link:../../private/fuman/packages/net + '@fuman/utils': + specifier: workspace:^ + version: link:../../private/fuman/packages/utils '@mtcute/file-id': specifier: workspace:^ version: link:../file-id @@ -298,6 +307,9 @@ importers: packages/node: dependencies: + '@fuman/node-net': + specifier: workspace:^ + version: link:../../private/fuman/packages/node-net '@mtcute/core': specifier: workspace:^ version: link:../core @@ -415,6 +427,9 @@ importers: packages/web: dependencies: + '@fuman/net': + specifier: workspace:^ + version: link:../../private/fuman/packages/net '@mtcute/core': specifier: workspace:^ version: link:../core @@ -6808,7 +6823,7 @@ snapshots: acorn: 8.12.1 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - semver: 7.5.1 + semver: 7.6.3 jsonfile@4.0.0: optionalDependencies: @@ -6897,7 +6912,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.6.0 + semver: 7.6.3 markdown-it@14.1.0: dependencies: @@ -7036,7 +7051,7 @@ snapshots: node-abi@3.15.0: dependencies: - semver: 7.5.1 + semver: 7.6.3 node-gyp-build@4.8.1: {} @@ -8031,7 +8046,7 @@ snapshots: espree: 9.6.1 esquery: 1.6.0 lodash: 4.17.21 - semver: 7.5.1 + semver: 7.6.3 transitivePeerDependencies: - supports-color diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e6912143..c460967d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - packages/* + - private/fuman/packages/* - '!e2e/*' diff --git a/scripts/publish.js b/scripts/publish.js index b551be96..7a4a60e5 100644 --- a/scripts/publish.js +++ b/scripts/publish.js @@ -130,6 +130,11 @@ function listPackages(all = false) { for (const f of fs.readdirSync(path.join(__dirname, '../packages'))) { if (f[0] === '.') continue + if (f === 'mtproxy' || f === 'socks-proxy' || f === 'http-proxy' || f === 'bun') { + // todo: return once we figure out fuman + continue + } + if (!all) { if (IS_JSR && JSR_EXCEPTIONS[f] === 'never') continue if (!IS_JSR && JSR_EXCEPTIONS[f] === 'only') continue diff --git a/scripts/validate-deps-versions.js b/scripts/validate-deps-versions.js index 80347398..4322df89 100644 --- a/scripts/validate-deps-versions.js +++ b/scripts/validate-deps-versions.js @@ -16,7 +16,8 @@ export async function validateDepsVersions() { if (!deps) return Object.entries(deps).forEach(([depName, depVersions]) => { - if (depName.startsWith('@mtcute/')) return + // todo: remove this when fuman/net is published + if (depName.startsWith('@mtcute/') || depName.startsWith('@fuman/')) return if (!versions[depName]) { versions[depName] = {}