feat: support http(s) proxies
This commit is contained in:
parent
649f86f3ed
commit
a46c6e8894
6 changed files with 213 additions and 6 deletions
|
@ -2,6 +2,10 @@ import { TransportFactory } from './abstract'
|
||||||
|
|
||||||
export * from './abstract'
|
export * from './abstract'
|
||||||
export * from './streamed'
|
export * from './streamed'
|
||||||
|
export * from './tcp'
|
||||||
|
export * from './tcp-intermediate'
|
||||||
|
export * from './websocket'
|
||||||
|
export * from './ws-obfuscated'
|
||||||
|
|
||||||
/** Platform-defined default transport factory */
|
/** Platform-defined default transport factory */
|
||||||
export let defaultTransportFactory: TransportFactory
|
export let defaultTransportFactory: TransportFactory
|
||||||
|
|
|
@ -13,12 +13,12 @@ const debug = require('debug')('mtcute:tcp')
|
||||||
export abstract class TcpTransport
|
export abstract class TcpTransport
|
||||||
extends EventEmitter
|
extends EventEmitter
|
||||||
implements ICuteTransport {
|
implements ICuteTransport {
|
||||||
private _currentDc: tl.RawDcOption | null = null
|
protected _currentDc: tl.RawDcOption | null = null
|
||||||
private _state: TransportState = TransportState.Idle
|
protected _state: TransportState = TransportState.Idle
|
||||||
private _socket: Socket | null = null
|
protected _socket: Socket | null = null
|
||||||
|
|
||||||
abstract _packetCodec: PacketCodec
|
abstract _packetCodec: PacketCodec
|
||||||
private _crypto: ICryptoProvider
|
protected _crypto: ICryptoProvider
|
||||||
|
|
||||||
packetCodecInitialized = false
|
packetCodecInitialized = false
|
||||||
|
|
||||||
|
|
167
packages/http-proxy/index.ts
Normal file
167
packages/http-proxy/index.ts
Normal file
|
@ -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<string, string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
17
packages/http-proxy/package.json
Normal file
17
packages/http-proxy/package.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "@mtcute/http-proxy",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "HTTP(S) proxy support for MTCute",
|
||||||
|
"author": "Alisa Sireneva <me@tei.su>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
19
packages/http-proxy/tsconfig.json
Normal file
19
packages/http-proxy/tsconfig.json
Normal file
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,9 +7,9 @@
|
||||||
"./src"
|
"./src"
|
||||||
],
|
],
|
||||||
"typedocOptions": {
|
"typedocOptions": {
|
||||||
"name": "@mtcute/client",
|
"name": "@mtcute/sqlite",
|
||||||
"includeVersion": true,
|
"includeVersion": true,
|
||||||
"out": "../../docs/packages/client",
|
"out": "../../docs/packages/sqlite",
|
||||||
"listInvalidSymbolLinks": true,
|
"listInvalidSymbolLinks": true,
|
||||||
"excludePrivate": true,
|
"excludePrivate": true,
|
||||||
"entryPoints": [
|
"entryPoints": [
|
||||||
|
|
Loading…
Reference in a new issue