168 lines
4.6 KiB
TypeScript
168 lines
4.6 KiB
TypeScript
|
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()
|
||
|
}
|