From a46c6e8894b9d5727eadc30dffc9071e0d74f5e8 Mon Sep 17 00:00:00 2001 From: teidesu Date: Sun, 23 May 2021 01:56:59 +0300 Subject: [PATCH] feat: support http(s) proxies --- packages/core/src/network/transports/index.ts | 4 + packages/core/src/network/transports/tcp.ts | 8 +- packages/http-proxy/index.ts | 167 ++++++++++++++++++ packages/http-proxy/package.json | 17 ++ packages/http-proxy/tsconfig.json | 19 ++ packages/sqlite/tsconfig.json | 4 +- 6 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 packages/http-proxy/index.ts create mode 100644 packages/http-proxy/package.json create mode 100644 packages/http-proxy/tsconfig.json diff --git a/packages/core/src/network/transports/index.ts b/packages/core/src/network/transports/index.ts index fa7be17f..a1b7510b 100644 --- a/packages/core/src/network/transports/index.ts +++ b/packages/core/src/network/transports/index.ts @@ -2,6 +2,10 @@ import { TransportFactory } from './abstract' export * from './abstract' export * from './streamed' +export * from './tcp' +export * from './tcp-intermediate' +export * from './websocket' +export * from './ws-obfuscated' /** Platform-defined default transport factory */ export let defaultTransportFactory: TransportFactory diff --git a/packages/core/src/network/transports/tcp.ts b/packages/core/src/network/transports/tcp.ts index 480a5d50..1dc9babd 100644 --- a/packages/core/src/network/transports/tcp.ts +++ b/packages/core/src/network/transports/tcp.ts @@ -13,12 +13,12 @@ const debug = require('debug')('mtcute:tcp') export abstract class TcpTransport extends EventEmitter implements ICuteTransport { - private _currentDc: tl.RawDcOption | null = null - private _state: TransportState = TransportState.Idle - private _socket: Socket | null = null + protected _currentDc: tl.RawDcOption | null = null + protected _state: TransportState = TransportState.Idle + protected _socket: Socket | null = null abstract _packetCodec: PacketCodec - private _crypto: ICryptoProvider + protected _crypto: ICryptoProvider packetCodecInitialized = false diff --git a/packages/http-proxy/index.ts b/packages/http-proxy/index.ts new file mode 100644 index 00000000..7031ff1d --- /dev/null +++ b/packages/http-proxy/index.ts @@ -0,0 +1,167 @@ +import { + IntermediatePacketCodec, + TcpTransport, + TransportState, +} from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { connect as connectTcp } from 'net' +import { connect as connectTls, SecureContextOptions } from 'tls' + +const debug = require('debug')('mtcute:http-proxy') + +/** + * An error has occurred while connecting to an HTTP(s) proxy + */ +export class HttpProxyConnectionError extends Error {} + +export interface HttpProxySettings { + /** + * Host of the proxy (e.g. `proxy.example.com`) + */ + 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 + + /** + * 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 + + /** + * 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 HttpProxiedTcpTransport extends TcpTransport { + readonly _proxy: HttpProxySettings + + constructor(proxy: HttpProxySettings) { + super() + this._proxy = proxy + } + + connect(dc: tl.RawDcOption): void { + if (this._state !== TransportState.Idle) + throw new Error('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 = 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)) + } + + private _onProxyConnected() { + 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}]` + + const headers = { + ...(this._proxy.headers ?? {}), + } + headers['Host'] = ip + + if (this._proxy.user) { + let auth = this._proxy.user + if (this._proxy.password) { + auth += ':' + this._proxy.password + } + headers['Proxy-Authorization'] = + 'Basic ' + Buffer.from(auth).toString('base64') + } + headers['Proxy-Connection'] = 'Keep-Alive' + + const packet = `CONNECT ${ip} HTTP/1.1${Object.keys(headers).map( + (k) => `\r\n${k}: ${headers[k]}` + )}\r\n\r\n` + + this._socket!.write(packet) + this._socket!.once('data', (msg) => { + debug( + '[%s:%d] CONNECT resulted in: %s', + this._proxy.host, + this._proxy.port, + msg + ) + + const [proto, code, name] = msg.toString().split(' ') + if (!proto.match(/^HTTP\/1.[01]$/i)) { + // wtf? + this._socket!.emit( + 'error', + new HttpProxyConnectionError( + `Server returned invalid protocol: ${proto}` + ) + ) + return + } + + if (code[0] !== '2') { + this._socket!.emit( + 'error', + new HttpProxyConnectionError( + `Server returned error: ${code} ${name}` + ) + ) + return + } + + // all ok, connection established, can now call handleConnect + this._socket!.on('data', (data) => this._packetCodec.feed(data)) + this.handleConnect() + }) + } +} + +export class HttpProxiedIntermediateTcpTransport extends HttpProxiedTcpTransport { + _packetCodec = new IntermediatePacketCodec() +} diff --git a/packages/http-proxy/package.json b/packages/http-proxy/package.json new file mode 100644 index 00000000..beba4288 --- /dev/null +++ b/packages/http-proxy/package.json @@ -0,0 +1,17 @@ +{ + "name": "@mtcute/http-proxy", + "private": true, + "version": "0.0.0", + "description": "HTTP(S) proxy support for MTCute", + "author": "Alisa Sireneva ", + "license": "MIT", + "main": "index.ts", + "scripts": { + "test": "mocha -r ts-node/register tests/**/*.spec.ts", + "docs": "npx typedoc", + "build": "tsc" + }, + "dependencies": { + "@mtcute/core": "^0.0.0" + } +} diff --git a/packages/http-proxy/tsconfig.json b/packages/http-proxy/tsconfig.json new file mode 100644 index 00000000..bbc65917 --- /dev/null +++ b/packages/http-proxy/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": [ + "./src" + ], + "typedocOptions": { + "name": "@mtcute/http-proxy", + "includeVersion": true, + "out": "../../docs/packages/http-proxy", + "listInvalidSymbolLinks": true, + "excludePrivate": true, + "entryPoints": [ + "./src/index.ts" + ] + } +} diff --git a/packages/sqlite/tsconfig.json b/packages/sqlite/tsconfig.json index 948bccf8..00a4b4e5 100644 --- a/packages/sqlite/tsconfig.json +++ b/packages/sqlite/tsconfig.json @@ -7,9 +7,9 @@ "./src" ], "typedocOptions": { - "name": "@mtcute/client", + "name": "@mtcute/sqlite", "includeVersion": true, - "out": "../../docs/packages/client", + "out": "../../docs/packages/sqlite", "listInvalidSymbolLinks": true, "excludePrivate": true, "entryPoints": [