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 './streamed'
|
||||
export * from './tcp'
|
||||
export * from './tcp-intermediate'
|
||||
export * from './websocket'
|
||||
export * from './ws-obfuscated'
|
||||
|
||||
/** Platform-defined default transport factory */
|
||||
export let defaultTransportFactory: TransportFactory
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
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"
|
||||
],
|
||||
"typedocOptions": {
|
||||
"name": "@mtcute/client",
|
||||
"name": "@mtcute/sqlite",
|
||||
"includeVersion": true,
|
||||
"out": "../../docs/packages/client",
|
||||
"out": "../../docs/packages/sqlite",
|
||||
"listInvalidSymbolLinks": true,
|
||||
"excludePrivate": true,
|
||||
"entryPoints": [
|
||||
|
|
Loading…
Reference in a new issue