mtcute/packages/mtproxy/index.ts
2023-11-09 00:20:43 +03:00

248 lines
7.7 KiB
TypeScript

/* eslint-disable no-restricted-globals */
// todo fixme
import { connect } from 'net'
import {
IntermediatePacketCodec,
IPacketCodec,
MtcuteError,
MtSecurityError,
MtUnsupportedError,
ObfuscatedPacketCodec,
PaddedIntermediatePacketCodec,
tl,
TransportState,
} from '@mtcute/core'
import { BaseTcpTransport } from '@mtcute/core/src/network/transports/tcp.js'
import { buffersEqual } from '@mtcute/core/utils.js'
import { FakeTlsPacketCodec, generateFakeTlsHeader } from './fake-tls.js'
/**
* MTProto proxy settings
*/
export interface MtProxySettings {
/**
* Host or IP of the proxy (e.g. `proxy.example.com`, `1.2.3.4`)
*/
host: string
/**
* Port of the proxy (e.g. `8888`)
*/
port: number
/**
* Secret of the proxy, optionally encoded either as hex or base64
*/
secret: string | Buffer
}
const MAX_DOMAIN_LENGTH = 182 // must be small enough not to overflow TLS-hello length
const TLS_START = [Buffer.from('160303', 'hex'), Buffer.from('140303000101170303', 'hex')]
/**
* TCP transport that connects via an MTProxy
*/
export class MtProxyTcpTransport extends BaseTcpTransport {
readonly _proxy: MtProxySettings
private _rawSecret: Buffer
private _randomPadding = false
private _fakeTlsDomain: string | null = null
/**
* @param proxy Information about the proxy
*/
constructor(proxy: MtProxySettings) {
super()
this._proxy = proxy
// validate and parse secret
let secret: Buffer
if (Buffer.isBuffer(proxy.secret)) {
secret = proxy.secret
} else if (proxy.secret.match(/^[0-9a-f]+$/i)) {
secret = Buffer.from(proxy.secret, 'hex')
} else {
secret = Buffer.from(proxy.secret, 'base64url')
}
if (secret.length > 17 + MAX_DOMAIN_LENGTH) {
throw new MtSecurityError('Invalid secret: too long')
}
if (secret.length < 16) {
throw new MtSecurityError('Invalid secret: too short')
}
if (secret.length === 16) {
this._rawSecret = secret
} else if (secret.length === 17 && secret[0] === 0xdd) {
this._rawSecret = secret.slice(1)
this._randomPadding = true
} else if (secret.length >= 18 && secret[0] === 0xee) {
this._rawSecret = secret.slice(1, 17)
this._fakeTlsDomain = secret.slice(17).toString()
} else {
throw new MtUnsupportedError('Unsupported secret')
}
}
getMtproxyInfo(): tl.RawInputClientProxy {
return {
_: 'inputClientProxy',
address: this._proxy.host,
port: this._proxy.port,
}
}
_packetCodec!: IPacketCodec
connect(dc: tl.RawDcOption, testMode: boolean): void {
if (this._state !== TransportState.Idle) {
throw new MtcuteError('Transport is not IDLE')
}
if (this._packetCodec && this._currentDc?.id !== dc.id) {
// dc changed, thus the codec's init will change too
// clean up to avoid memory leaks
this.packetCodecInitialized = false
this._packetCodec.reset()
this._packetCodec.removeAllListeners()
delete (this as Partial<MtProxyTcpTransport>)._packetCodec
}
if (!this._packetCodec) {
const proxy = {
dcId: dc.id,
media: dc.mediaOnly!,
test: testMode,
secret: this._rawSecret,
}
if (!this._fakeTlsDomain) {
let inner: IPacketCodec
if (this._randomPadding) {
inner = new PaddedIntermediatePacketCodec()
} else {
inner = new IntermediatePacketCodec()
}
this._packetCodec = new ObfuscatedPacketCodec(inner, proxy)
} else {
this._packetCodec = new FakeTlsPacketCodec(
new ObfuscatedPacketCodec(new PaddedIntermediatePacketCodec(), proxy),
)
}
this._packetCodec.setup?.(this._crypto, this.log)
this._packetCodec.on('error', (err) => this.emit('error', err))
this._packetCodec.on('packet', (buf) => this.emit('message', buf))
}
this._state = TransportState.Connecting
this._currentDc = dc
if (this._fakeTlsDomain) {
this._socket = connect(
this._proxy.port,
this._proxy.host,
// MTQ-55
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this._handleConnectFakeTls.bind(this),
)
} else {
this._socket = connect(
this._proxy.port,
this._proxy.host,
// MTQ-55
this.handleConnect.bind(this),
)
this._socket.on('data', (data) => this._packetCodec.feed(data))
}
this._socket.on('error', this.handleError.bind(this))
this._socket.on('close', this.close.bind(this))
}
private async _handleConnectFakeTls(): Promise<void> {
try {
const hello = await generateFakeTlsHeader(this._fakeTlsDomain!, this._rawSecret, this._crypto)
const helloRand = hello.slice(11, 11 + 32)
let serverHelloBuffer: Buffer | null = null
const checkHelloResponse = async (buf: Buffer): Promise<boolean> => {
if (serverHelloBuffer) {
buf = Buffer.concat([serverHelloBuffer, buf])
}
const resp = buf
for (const first of TLS_START) {
if (buf.length < first.length + 2) {
throw new MtSecurityError('Server hello is too short')
}
if (!buffersEqual(buf.slice(0, first.length), first)) {
throw new MtSecurityError('Server hello is invalid')
}
buf = buf.slice(first.length)
const skipSize = buf.readUInt16BE()
buf = buf.slice(2)
if (buf.length < skipSize) {
// likely got split into multiple packets
if (serverHelloBuffer) {
throw new MtSecurityError('Server hello is too short')
}
serverHelloBuffer = resp
return false
}
buf = buf.slice(skipSize)
}
const respRand = resp.slice(11, 11 + 32)
const hash = await this._crypto.hmacSha256(
Buffer.concat([helloRand, resp.slice(0, 11), Buffer.alloc(32, 0), resp.slice(11 + 32)]),
this._rawSecret,
)
if (!buffersEqual(hash, respRand)) {
throw new MtSecurityError('Response hash is invalid')
}
return true
}
const packetHandler = (buf: Buffer): void => {
checkHelloResponse(buf)
.then((done) => {
if (!done) return
this._socket!.off('data', packetHandler)
this._socket!.on('data', (data) => {
this._packetCodec.feed(data)
})
return this.handleConnect()
})
.catch((err) => this._socket!.emit('error', err))
}
this._socket!.write(hello)
this._socket!.on('data', packetHandler)
} catch (e) {
this._socket!.emit('error', e)
}
}
}