feat: support http(s) proxies

This commit is contained in:
teidesu 2021-05-23 01:56:59 +03:00
parent 649f86f3ed
commit a46c6e8894
6 changed files with 213 additions and 6 deletions

View file

@ -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

View file

@ -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

View 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()
}

View 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"
}
}

View 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"
]
}
}

View file

@ -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": [