chore!: migrated proxies to fuman

breaking: http-proxy, socks-proxy and mtproxy packages are deprecated, proxified implementations are available in runtime-specific packages
This commit is contained in:
alina 🌸 2024-09-05 03:00:21 +03:00
parent d512da6831
commit baef78403e
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
52 changed files with 1083 additions and 2080 deletions

View file

@ -1,7 +1,7 @@
{
"name": "mtcute-workspace",
"type": "module",
"version": "0.16.13",
"version": "0.17.0-fuman-alpha",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "Type-safe library for MTProto (Telegram API) for browser and NodeJS",

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/bun",
"type": "module",
"version": "0.16.9",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Meta-package for Bun",
"author": "alina sireneva <alina@tei.su>",
@ -20,7 +20,10 @@
"@mtcute/core": "workspace:^",
"@mtcute/html-parser": "workspace:^",
"@mtcute/markdown-parser": "workspace:^",
"@mtcute/wasm": "workspace:^"
"@mtcute/wasm": "workspace:^",
"@fuman/bun-net": "workspace:^",
"@fuman/net": "workspace:^",
"@fuman/io": "workspace:^"
},
"devDependencies": {
"@mtcute/test": "workspace:^"

View file

@ -18,7 +18,7 @@ import { downloadAsNodeStream } from './methods/download-node-stream.js'
import { BunPlatform } from './platform.js'
import { SqliteStorage } from './sqlite/index.js'
import { BunCryptoProvider } from './utils/crypto.js'
// import { TcpTransport } from './utils/tcp.js'
import { TcpTransport } from './utils/tcp.js'
export type { TelegramClientOptions }
@ -49,7 +49,7 @@ export class BaseTelegramClient extends BaseTelegramClientBase {
super({
crypto: new BunCryptoProvider(),
transport: {} as any, // todo
transport: TcpTransport,
...opts,
storage:
typeof opts.storage === 'string'

View file

@ -0,0 +1,74 @@
import type { HttpProxySettings as FumanHttpProxySettings, ITcpConnection, SocksProxySettings, TcpEndpoint } from '@fuman/net'
import { performHttpProxyHandshake, performSocksHandshake } from '@fuman/net'
import { connectTcp, connectTls } from '@fuman/bun-net'
import { BaseMtProxyTransport, type ITelegramConnection, IntermediatePacketCodec, type TelegramTransport } from '@mtcute/core'
import type { BasicDcOption } from '@mtcute/core/utils.js'
export type { SocksProxySettings } from '@fuman/net'
export { SocksProxyConnectionError, HttpProxyConnectionError } from '@fuman/net'
export type { MtProxySettings } from '@mtcute/core'
export interface HttpProxySettings extends FumanHttpProxySettings {
/**
* Whether this is a HTTPS proxy (by default it is regular HTTP).
*/
tls?: boolean
}
export class HttpProxyTcpTransport implements TelegramTransport {
constructor(readonly proxy: HttpProxySettings) {}
async connect(dc: BasicDcOption): Promise<ITelegramConnection> {
let conn
if (this.proxy.tls) {
conn = await connectTls({
address: this.proxy.host,
port: this.proxy.port,
})
} else {
conn = await connectTcp({
address: this.proxy.host,
port: this.proxy.port,
})
}
await performHttpProxyHandshake(conn, conn, this.proxy, {
address: dc.ipAddress,
port: dc.port,
})
return conn
}
packetCodec(): IntermediatePacketCodec {
return new IntermediatePacketCodec()
}
}
export class SocksProxyTcpTransport implements TelegramTransport {
constructor(readonly proxy: SocksProxySettings) {}
async connect(dc: BasicDcOption): Promise<ITelegramConnection> {
const conn = await connectTcp({
address: this.proxy.host,
port: this.proxy.port,
})
await performSocksHandshake(conn, conn, this.proxy, {
address: dc.ipAddress,
port: dc.port,
})
return conn
}
packetCodec(): IntermediatePacketCodec {
return new IntermediatePacketCodec()
}
}
export class MtProxyTcpTransport extends BaseMtProxyTransport {
async _connectTcp(endpoint: TcpEndpoint): Promise<ITcpConnection> {
return connectTcp(endpoint)
}
}

View file

@ -1,152 +1,7 @@
// todo
// import EventEmitter from 'node:events'
import { connectTcp } from '@fuman/bun-net'
import { IntermediatePacketCodec, type TelegramTransport } from '@mtcute/core'
// import { IntermediatePacketCodec, IPacketCodec, ITelegramConnection, MtcuteError } from '@mtcute/core'
// import { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js'
// /**
// * Base for TCP transports.
// * Subclasses must provide packet codec in `_packetCodec` property
// */
// export abstract class BaseTcpTransport extends EventEmitter implements ITelegramConnection {
// protected _currentDc: BasicDcOption | null = null
// protected _state: TransportState = TransportState.Idle
// protected _socket: Socket | null = null
// abstract _packetCodec: IPacketCodec
// protected _crypto!: ICryptoProvider
// protected log!: Logger
// packetCodecInitialized = false
// private _updateLogPrefix() {
// if (this._currentDc) {
// this.log.prefix = `[TCP:${this._currentDc.ipAddress}:${this._currentDc.port}] `
// } else {
// this.log.prefix = '[TCP:disconnected] '
// }
// }
// setup(crypto: ICryptoProvider, log: Logger): void {
// this._crypto = crypto
// this.log = log.create('tcp')
// this._updateLogPrefix()
// }
// state(): TransportState {
// return this._state
// }
// currentDc(): BasicDcOption | null {
// return this._currentDc
// }
// // eslint-disable-next-line unused-imports/no-unused-vars
// connect(dc: BasicDcOption, testMode: boolean): void {
// if (this._state !== TransportState.Idle) {
// throw new MtcuteError('Transport is not IDLE')
// }
// if (!this.packetCodecInitialized) {
// 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.packetCodecInitialized = true
// }
// this._state = TransportState.Connecting
// this._currentDc = dc
// this._updateLogPrefix()
// this.log.debug('connecting to %j', dc)
// Bun.connect({
// hostname: dc.ipAddress,
// port: dc.port,
// socket: {
// open: this.handleConnect.bind(this),
// error: this.handleError.bind(this),
// data: (socket, data) => this._packetCodec.feed(data),
// close: this.close.bind(this),
// drain: this.handleDrained.bind(this),
// },
// }).catch((err) => {
// this.handleError(null, err as Error)
// this.close()
// })
// }
// close(): void {
// if (this._state === TransportState.Idle) return
// this.log.info('connection closed')
// this._state = TransportState.Idle
// this._socket?.end()
// this._socket = null
// this._currentDc = null
// this._packetCodec.reset()
// this._sendOnceDrained = []
// this.emit('close')
// }
// handleError(socket: unknown, error: Error): void {
// this.log.error('error: %s', error.stack)
// if (this.listenerCount('error') > 0) {
// this.emit('error', error)
// }
// }
// handleConnect(socket: Socket): void {
// this._socket = socket
// this.log.info('connected')
// Promise.resolve(this._packetCodec.tag())
// .then((initialMessage) => {
// if (initialMessage.length) {
// this._socket!.write(initialMessage)
// this._state = TransportState.Ready
// this.emit('ready')
// } else {
// this._state = TransportState.Ready
// this.emit('ready')
// }
// })
// .catch((err) => {
// if (this.listenerCount('error') > 0) {
// this.emit('error', err)
// }
// })
// }
// async send(bytes: Uint8Array): Promise<void> {
// const framed = await this._packetCodec.encode(bytes)
// if (this._state !== TransportState.Ready) {
// throw new MtcuteError('Transport is not READY')
// }
// const written = this._socket!.write(framed)
// if (written < framed.length) {
// this._sendOnceDrained.push(framed.subarray(written))
// }
// }
// private _sendOnceDrained: Uint8Array[] = []
// private handleDrained(): void {
// while (this._sendOnceDrained.length) {
// const data = this._sendOnceDrained.shift()!
// const written = this._socket!.write(data)
// if (written < data.length) {
// this._sendOnceDrained.unshift(data.subarray(written))
// break
// }
// }
// }
// }
// export class TcpTransport extends BaseTcpTransport {
// _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec()
// }
export const TcpTransport: TelegramTransport = {
connect: dc => connectTcp({ address: dc.ipAddress, port: dc.port }),
packetCodec: () => new IntermediatePacketCodec(),
}

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/convert",
"type": "module",
"version": "0.16.9",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Cross-library session conversion utilities",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/core",
"type": "module",
"version": "0.16.13",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Type-safe library for MTProto (Telegram API)",
"author": "alina sireneva <alina@tei.su>",

View file

@ -8,7 +8,6 @@ import type { CurrentUserInfo } from '../storage/service/current-user.js'
export interface StringSessionData {
version: number
testMode: boolean
primaryDcs: DcOptions
self?: CurrentUserInfo | null
authKey: Uint8Array
@ -54,10 +53,6 @@ export function writeStringSession(data: StringSessionData): string {
flags |= 1
}
if (data.testMode) {
flags |= 2
}
writer.uint8View[0] = version
writer.pos += 1
@ -104,7 +99,7 @@ export function readStringSession(data: string): StringSessionData {
const flags = reader.int()
const hasSelf = flags & 1
const testMode = Boolean(flags & 2)
const testModeOld = Boolean(flags & 2)
const hasMedia = version >= 2 && Boolean(flags & 4)
let primaryDc: BasicDcOption
@ -135,6 +130,13 @@ export function readStringSession(data: string): StringSessionData {
throw new Error('unreachable')
}
if (testModeOld) {
primaryDc.testMode = true
primaryMediaDc.testMode = true
} else if (primaryDc.testMode !== primaryMediaDc.testMode) {
throw new MtArgumentError('Primary DC and primary media DC must have the same test mode flag')
}
let self: CurrentUserInfo | null = null
if (hasSelf) {
@ -154,7 +156,6 @@ export function readStringSession(data: string): StringSessionData {
return {
version,
testMode,
primaryDcs: {
main: primaryDc,
media: primaryMediaDc,

View file

@ -7,6 +7,6 @@ export type {
RpcCallMiddlewareContext,
RpcCallOptions,
} from './network-manager.js'
// export * from './reconnection.js'
export * from './mtproxy/index.js'
export * from './session-connection.js'
export * from './transports/index.js'

View file

@ -0,0 +1,300 @@
import { Bytes, type ISyncWritable, read } from '@fuman/io'
import type { Logger } from '../../utils'
import { concatBuffers, dataViewFromBuffer } from '../../utils'
import { bigIntModInv, bigIntModPow, bigIntToBuffer, bufferToBigInt } from '../../utils/bigint-utils'
import type { ICryptoProvider } from '../../utils/crypto/abstract'
import type { IPacketCodec } from '../transports'
const MAX_TLS_PACKET_LENGTH = 2878
// ref: https://github.com/tdlib/td/blob/master/td/mtproto/TlsInit.cpp
const KEY_MOD = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEDn
// 2^255 - 19
const QUAD_RES_MOD = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEDn
// (mod - 1) / 2 = 2^254 - 10
const QUAD_RES_POW = 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6n
function _getY2(x: bigint, mod: bigint): bigint {
// returns y = x^3 + x^2 * 486662 + x
let y = x
y = (y + 486662n) % mod
y = (y * x) % mod
y = (y + 1n) % mod
y = (y * x) % mod
return y
}
function _getDoubleX(x: bigint, mod: bigint): bigint {
// returns x_2 = (x^2 - 1)^2/(4*y^2)
let denominator = _getY2(x, mod)
denominator = (denominator * 4n) % mod
let numerator = (x * x) % mod
numerator = (numerator - 1n) % mod
numerator = (numerator * numerator) % mod
denominator = bigIntModInv(denominator, mod)
numerator = (numerator * denominator) % mod
return numerator
}
function _isQuadraticResidue(a: bigint): boolean {
const r = bigIntModPow(a, QUAD_RES_POW, QUAD_RES_MOD)
return r === 1n
}
function executeTlsOperations(h: TlsHelloWriter): void {
h.string(new Uint8Array([0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xFC, 0x03, 0x03]))
h.zero(32)
h.string(new Uint8Array([0x20]))
h.random(32)
h.string(new Uint8Array([0x00, 0x20]))
h.grease(0)
/* eslint-disable antfu/consistent-list-newline */
h.string(new Uint8Array([
0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xC0, 0x2B, 0xC0, 0x2F, 0xC0, 0x2C,
0xC0, 0x30, 0xCC, 0xA9, 0xCC, 0xA8, 0xC0, 0x13, 0xC0, 0x14, 0x00, 0x9C,
0x00, 0x9D, 0x00, 0x2F, 0x00, 0x35, 0x01, 0x00, 0x01, 0x93,
]))
h.grease(2)
h.string(new Uint8Array([0x00, 0x00, 0x00, 0x00]))
h.beginScope()
h.beginScope()
h.string(new Uint8Array([0x00]))
h.beginScope()
h.domain()
h.endScope()
h.endScope()
h.endScope()
h.string(new Uint8Array([0x00, 0x17, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x08]))
h.grease(4)
h.string(
new Uint8Array([
0x00, 0x1D, 0x00, 0x17, 0x00, 0x18, 0x00, 0x0B, 0x00, 0x02, 0x01, 0x00,
0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0E, 0x00, 0x0C, 0x02, 0x68,
0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2F, 0x31, 0x2E, 0x31, 0x00, 0x05,
0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x12, 0x00,
0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05,
0x01, 0x08, 0x06, 0x06, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00,
0x2B, 0x00, 0x29,
]),
)
h.grease(4)
h.string(new Uint8Array([0x00, 0x01, 0x00, 0x00, 0x1D, 0x00, 0x20]))
h.key()
h.string(new Uint8Array([0x00, 0x2D, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2B, 0x00, 0x0B, 0x0A]))
h.grease(6)
h.string(new Uint8Array([0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1B, 0x00, 0x03, 0x02, 0x00, 0x02]))
h.grease(3)
h.string(new Uint8Array([0x00, 0x01, 0x00, 0x00, 0x15]))
/* eslint-enable */
}
function initGrease(crypto: ICryptoProvider, size: number): Uint8Array {
const buf = crypto.randomBytes(size)
for (let i = 0; i < size; i++) {
buf[i] = (buf[i] & 0xF0) + 0x0A
}
for (let i = 1; i < size; i += 2) {
if (buf[i] === buf[i - 1]) {
buf[i] ^= 0x10
}
}
return buf
}
class TlsHelloWriter {
buf: Uint8Array
dv: DataView
pos = 0
private _domain: Uint8Array
private _grease
private _scopes: number[] = []
constructor(
readonly crypto: ICryptoProvider,
size: number,
domain: Uint8Array,
) {
this._domain = domain
this.buf = new Uint8Array(size)
this.dv = dataViewFromBuffer(this.buf)
this._grease = initGrease(this.crypto, 7)
}
string(buf: Uint8Array) {
this.buf.set(buf, this.pos)
this.pos += buf.length
}
random(size: number) {
this.string(this.crypto.randomBytes(size))
}
zero(size: number) {
// since the Uint8Array is initialized with zeros, we can just skip
this.pos += size
}
domain() {
this.string(this._domain)
}
grease(seed: number) {
this.buf[this.pos] = this.buf[this.pos + 1] = this._grease[seed]
this.pos += 2
}
key() {
for (;;) {
const key = this.crypto.randomBytes(32)
key[31] &= 127
let x = bufferToBigInt(key)
const y = _getY2(x, KEY_MOD)
if (_isQuadraticResidue(y)) {
for (let i = 0; i < 3; i++) {
x = _getDoubleX(x, KEY_MOD)
}
const key = bigIntToBuffer(x, 32, true)
this.string(key)
return
}
}
}
beginScope() {
this._scopes.push(this.pos)
this.pos += 2
}
endScope() {
const begin = this._scopes.pop()
if (begin === undefined) {
throw new Error('endScope called without beginScope')
}
const end = this.pos
const size = end - begin - 2
this.dv.setUint16(begin, size)
}
async finish(secret: Uint8Array): Promise<Uint8Array> {
const padSize = 515 - this.pos
const unixTime = ~~(Date.now() / 1000)
this.beginScope()
this.zero(padSize)
this.endScope()
const hash = await this.crypto.hmacSha256(this.buf, secret)
const dv = dataViewFromBuffer(hash)
const old = dv.getInt32(28, true)
dv.setInt32(28, old ^ unixTime, true)
this.buf.set(hash, 11)
return this.buf
}
}
export async function generateFakeTlsHeader(
domain: Uint8Array,
secret: Uint8Array,
crypto: ICryptoProvider,
): Promise<Uint8Array> {
const writer = new TlsHelloWriter(crypto, 517, domain)
executeTlsOperations(writer)
return writer.finish(secret)
}
/**
* Fake TLS packet codec, used for some MTProxies.
*
* Must only be used inside {@link MtProxyTcpTransport}
* @internal
*/
export class FakeTlsPacketCodec implements IPacketCodec {
// protected _stream: Buffer = Buffer.alloc(0)
constructor(readonly _inner: IPacketCodec) {}
private _isFirstTls = true
setup?(crypto: ICryptoProvider, log: Logger): void {
this._inner.setup?.(crypto, log)
}
private _tag!: Uint8Array
async tag(): Promise<Uint8Array> {
this._tag = await this._inner.tag()
return new Uint8Array(0)
}
async encode(packet: Uint8Array, into: ISyncWritable): Promise<void> {
const tmp = Bytes.alloc(packet.length)
await this._inner.encode(packet, tmp)
while (tmp.available > 0) {
const header = new Uint8Array([0x17, 0x03, 0x03, 0x00, 0x00])
let packet
if (this._isFirstTls) {
this._isFirstTls = false
packet = concatBuffers([this._tag, tmp.readSync(MAX_TLS_PACKET_LENGTH - this._tag.length)])
} else {
packet = tmp.readSync(MAX_TLS_PACKET_LENGTH)
}
dataViewFromBuffer(header).setUint16(3, packet.length)
into.writeSync(header.length).set(header)
into.writeSync(packet.length).set(packet)
into.disposeWriteSync()
}
}
async decode(reader: Bytes, eof: boolean): Promise<Uint8Array | null> {
if (eof) return null
if (reader.available < 5) return null
const header = reader.readSync(3)
if (header[0] !== 0x17 || header[1] !== 0x03 || header[2] !== 0x03) {
throw new Error('Invalid TLS header')
}
const length = read.uint16be(reader)
if (length < reader.available - 5) {
reader.rewind(5)
return null
}
const packet = reader.readSync(length)
const inner = await this._inner.decode(Bytes.from(packet), eof)
if (!inner) {
reader.rewind(5 + length)
return null
}
return inner
}
reset(): void {
this._isFirstTls = true
}
}

View file

@ -0,0 +1,184 @@
import type { tl } from '@mtcute/tl'
import type { ITcpConnection, TcpEndpoint } from '@fuman/net'
import { Bytes, read, write } from '@fuman/io'
import { base64, hex } from '@fuman/utils'
import type { IPacketCodec, ITelegramConnection, MtProxyInfo, TelegramTransport } from '../transports/index.js'
import { IntermediatePacketCodec, ObfuscatedPacketCodec, PaddedIntermediatePacketCodec } from '../transports/index.js'
import { type BasicDcOption, type ICryptoProvider, type Logger, buffersEqual, concatBuffers, dataViewFromBuffer } from '../../utils/index.js'
import { MtSecurityError, MtUnsupportedError } from '../../types/errors.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 | Uint8Array
}
const MAX_DOMAIN_LENGTH = 182 // must be small enough not to overflow TLS-hello length
const TLS_START_SERVER = [
new Uint8Array([0x16, 0x03, 0x03]),
new Uint8Array([0x14, 0x03, 0x03, 0x00, 0x01, 0x01, 0x17, 0x03, 0x03]),
]
const TLS_START_CLIENT = new Uint8Array([0x14, 0x03, 0x03, 0x00, 0x01, 0x01])
/**
* Base for a TCP transport that connects via an MTProxy
*/
export abstract class BaseMtProxyTransport implements TelegramTransport {
abstract _connectTcp(endpoint: TcpEndpoint): Promise<ITcpConnection>
readonly _proxy: MtProxySettings
private _rawSecret: Uint8Array
private _randomPadding = false
private _fakeTlsDomain: Uint8Array | null = null
private _crypto!: ICryptoProvider
private _log!: Logger
setup(crypto: ICryptoProvider, log: Logger): void {
this._crypto = crypto
this._log = log.create('mtproxy')
}
/**
* @param proxy Information about the proxy
*/
constructor(proxy: MtProxySettings) {
this._proxy = proxy
// validate and parse secret
let secret: Uint8Array
if (ArrayBuffer.isView(proxy.secret)) {
secret = proxy.secret
} else if (proxy.secret.match(/^[0-9a-f]+$/i)) {
secret = hex.decode(proxy.secret)
} else {
secret = base64.decode(proxy.secret, true)
}
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)
} else {
throw new MtUnsupportedError('Unsupported secret')
}
}
getMtproxyInfo(): tl.RawInputClientProxy {
return {
_: 'inputClientProxy',
address: this._proxy.host,
port: this._proxy.port,
}
}
packetCodec(dc: BasicDcOption): IPacketCodec {
const proxy: MtProxyInfo = {
dcId: dc.id,
media: Boolean(dc.mediaOnly),
test: Boolean(dc.testMode),
secret: this._rawSecret,
}
if (!this._fakeTlsDomain) {
let inner: IPacketCodec
if (this._randomPadding) {
inner = new PaddedIntermediatePacketCodec()
} else {
inner = new IntermediatePacketCodec()
}
return new ObfuscatedPacketCodec(inner, proxy)
}
return new FakeTlsPacketCodec(
new ObfuscatedPacketCodec(new PaddedIntermediatePacketCodec(), proxy),
)
}
async connect(): Promise<ITelegramConnection> {
const conn = await this._connectTcp({
address: this._proxy.host,
port: this._proxy.port,
})
if (this._fakeTlsDomain) {
await this._handleConnectFakeTls(conn)
}
return conn
}
private async _handleConnectFakeTls(conn: ITcpConnection): Promise<void> {
this._log.debug('performing initial faketls handshake')
const hello = await generateFakeTlsHeader(this._fakeTlsDomain!, this._rawSecret, this._crypto)
const helloRand = hello.slice(11, 11 + 32)
await conn.write(hello)
const resp = Bytes.alloc(0)
for (const first of TLS_START_SERVER) {
const buf = await read.async.exactly(conn, first.length + 2)
write.bytes(resp, buf)
if (!buffersEqual(buf.slice(0, first.length), first)) {
throw new MtSecurityError('Server hello is invalid')
}
const skipSize = dataViewFromBuffer(buf).getUint16(first.length)
write.bytes(resp, await read.async.exactly(conn, skipSize))
}
const respBuf = resp.result()
const respRand = respBuf.slice(11, 11 + 32)
const hash = await this._crypto.hmacSha256(
concatBuffers([
helloRand,
respBuf.slice(0, 11),
new Uint8Array(32),
respBuf.slice(11 + 32),
]),
this._rawSecret,
)
if (!buffersEqual(hash, respRand)) {
throw new MtSecurityError('Response hash is invalid')
}
await conn.write(TLS_START_CLIENT)
this._log.debug('faketls handshake done')
}
}

View file

@ -30,18 +30,12 @@ export abstract class PersistentConnection extends EventEmitter {
private _uid = nextConnectionUid++
readonly params: PersistentConnectionParams
// protected _transport!: ITelegramConnection
private _sendOnceConnected: Uint8Array[] = []
private _codec: IPacketCodec
private _fuman: FumanPersistentConnection<BasicDcOption, ITelegramConnection>
// reconnection
private _lastError: Error | null = null
private _consequentFails = 0
private _previousWait: number | null = null
private _reconnectionTimeout: timers.Timer | null = null
private _shouldReconnectImmediately = false
protected _disconnectedManually = false
// inactivity timeout
@ -64,19 +58,41 @@ export abstract class PersistentConnection extends EventEmitter {
) {
super()
this.params = params
this.log.prefix = `[UID ${this._uid}] `
this._codec = this.params.transport.packetCodec()
this.params.transport.setup?.(this.params.crypto, log)
this._codec = this.params.transport.packetCodec(params.dc)
this._codec.setup?.(this.params.crypto, this.log)
this._onInactivityTimeout = this._onInactivityTimeout.bind(this)
this._fuman = new FumanPersistentConnection({
connect: dc => params.transport.connect(dc, params.testMode),
connect: (dc) => {
this._updateLogPrefix()
this.log.debug('connecting to %j', dc)
return params.transport.connect(dc, params.testMode)
},
onOpen: this._onOpen.bind(this),
onClose: this._onClose.bind(this),
onError: this._onError.bind(this),
onWait: wait => this.emit('wait', wait),
onWait: (wait) => {
this._updateLogPrefix()
this.log.debug('waiting for %d ms before reconnecting', wait)
this.emit('wait', wait)
},
})
this._updateLogPrefix()
}
private _updateLogPrefix() {
const uidPrefix = `[UID ${this._uid}] `
if (this._fuman.isConnected) {
const dc = this.params.dc
this.log.prefix = `${uidPrefix}[DC ${dc.id}:${dc.ipAddress}:${dc.port}] `
} else if (this._fuman.isConnecting) {
this.log.prefix = `${uidPrefix}[connecting] `
} else {
this.log.prefix = `${uidPrefix}[disconnected] `
}
}
get isConnected(): boolean {
@ -86,7 +102,13 @@ export abstract class PersistentConnection extends EventEmitter {
private _writer?: FramedWriter
private async _onOpen(conn: ITelegramConnection) {
await conn.write(await this._codec.tag())
this._updateLogPrefix()
this.log.debug('connected')
const tag = await this._codec.tag()
if (tag) {
await conn.write(tag)
}
const reader = new FramedReader(conn, this._codec)
this._writer = new FramedWriter(conn, this._codec)
@ -116,181 +138,45 @@ export abstract class PersistentConnection extends EventEmitter {
}
private async _onClose() {
this.log.debug('connection closed')
this._updateLogPrefix()
this._writer = undefined
this._codec.reset()
this.onClosed()
}
private async _onError(err: Error) {
this._lastError = err
this._updateLogPrefix()
this.onError(err)
}
async changeTransport(transport: TelegramTransport): Promise<void> {
await this._fuman.close()
this._codec = transport.packetCodec()
this._codec = transport.packetCodec(this.params.dc)
this._codec.setup?.(this.params.crypto, this.log)
await this._fuman.changeTransport(() => transport.connect(this.params.dc, this.params.testMode))
await this._fuman.changeTransport(dc => transport.connect(dc, this.params.testMode))
this._fuman.connect(this.params.dc)
// if (this._transport) {
// Promise.resolve(this._transport.close()).catch((err) => {
// this.log.warn('error closing previous transport: %e', err)
// })
// }
// this._transport = conn()
// this._transport.setup?.(this.params.crypto, this.log)
// this._transport.on('ready', this.onTransportReady.bind(this))
// this._transport.on('message', this.onMessage.bind(this))
// this._transport.on('error', this.onTransportError.bind(this))
// this._transport.on('close', this.onTransportClose.bind(this))
}
// onTransportReady(): void {
// // transport ready does not mean actual mtproto is ready
// if (this._sendOnceConnected.length) {
// const sendNext = () => {
// if (!this._sendOnceConnected.length) {
// this.onConnected()
// return
// }
// const data = this._sendOnceConnected.shift()!
// this._transport
// .send(data)
// .then(sendNext)
// .catch((err) => {
// this.log.error('error sending queued data: %e', err)
// this._sendOnceConnected.unshift(data)
// })
// }
// sendNext()
// return
// }
// this.onConnected()
// }
// protected onConnectionUsable(): void {
// const isReconnection = this._consequentFails > 0
// // reset reconnection related state
// this._lastError = null
// this._consequentFails = 0
// this._previousWait = null
// this._usable = true
// this.emit('usable', isReconnection)
// this._rescheduleInactivity()
// }
// onTransportError(err: Error): void {
// // transport is expected to emit `close` after `error`
// }
// onTransportClose(): void {
// // transport closed because of inactivity
// // obviously we dont want to reconnect then
// if (this._inactive || this._disconnectedManually) return
// if (this._shouldReconnectImmediately) {
// this._shouldReconnectImmediately = false
// this.connect()
// return
// }
// this._consequentFails += 1
// const wait = this.params.reconnectionStrategy(
// this.params,
// this._lastError,
// this._consequentFails,
// this._previousWait,
// )
// if (wait === false) {
// this.destroy().catch((err) => {
// this.log.warn('error destroying connection: %e', err)
// })
// return
// }
// this._previousWait = wait
// if (this._reconnectionTimeout != null) {
// timers.clearTimeout(this._reconnectionTimeout)
// }
// this._reconnectionTimeout = timers.setTimeout(() => {
// if (this._destroyed) return
// this._reconnectionTimeout = null
// this.connect()
// }, wait)
// }
connect(): void {
this._fuman.connect(this.params.dc)
this._inactive = false
// if (this.isConnected) {
// throw new MtcuteError('Connection is already opened!')
// }
// if (this._destroyed) {
// throw new MtcuteError('Connection is already destroyed!')
// }
// if (this._reconnectionTimeout != null) {
// clearTimeout(this._reconnectionTimeout)
// this._reconnectionTimeout = null
// }
// this._inactive = false
// this._disconnectedManually = false
// this._transport.connect(this.params.dc, this.params.testMode)
}
reconnect(): void {
// if (this._inactive) return
// // if we are already connected
// if (this.isConnected) {
// this._shouldReconnectImmediately = true
// Promise.resolve(this._transport.close()).catch((err) => {
// this.log.error('error closing transport: %e', err)
// })
// return
// }
// // if reconnection timeout is pending, it will be cancelled in connect()
// this.connect()
this._fuman.reconnect(true)
}
async disconnectManual(): Promise<void> {
// this._disconnectedManually = true
// await this._transport.close()
await this._fuman.close()
}
async destroy(): Promise<void> {
// if (this._reconnectionTimeout != null) {
// clearTimeout(this._reconnectionTimeout)
// }
// if (this._inactivityTimeout != null) {
// clearTimeout(this._inactivityTimeout)
// }
await this._fuman.close()
// this._transport.removeAllListeners()
// this._destroyed = true
}
protected _rescheduleInactivity(): void {

View file

@ -21,8 +21,9 @@ export interface ITelegramConnection extends IConnection<any, any> {
}
export interface TelegramTransport {
setup?(crypto: ICryptoProvider, log: Logger): void
connect: (dc: BasicDcOption, testMode: boolean) => Promise<ITelegramConnection>
packetCodec: () => IPacketCodec
packetCodec: (dc: BasicDcOption) => IPacketCodec
}
/**

View file

@ -33,7 +33,7 @@ export class IntermediatePacketCodec implements IPacketCodec {
}
if (reader.available < length) {
reader.unread(4)
reader.rewind(4)
return null
}

View file

@ -27,7 +27,6 @@ export class ObfuscatedPacketCodec implements IPacketCodec {
}
constructor(inner: IPacketCodec, proxy?: MtProxyInfo) {
// super(inner)
this._inner = inner
this._proxy = proxy
}
@ -99,19 +98,25 @@ export class ObfuscatedPacketCodec implements IPacketCodec {
async encode(packet: Uint8Array, into: ISyncWritable): Promise<void> {
const temp = Bytes.alloc(packet.length)
await this._inner.encode(packet, into)
await this._inner.encode(packet, temp)
write.bytes(into, this._encryptor!.process(temp.result()))
}
private _decodeBuf = Bytes.alloc()
async decode(reader: Bytes, eof: boolean): Promise<Uint8Array | null> {
const inner = await this._inner.decode(reader, eof)
if (!inner) return null
if (eof) return null
return this._decryptor!.process(inner)
if (reader.available > 0) {
const into = this._decodeBuf.writeSync(reader.available)
into.set(this._decryptor!.process(reader.readSync(reader.available)))
}
return this._inner.decode(this._decodeBuf, eof)
}
reset(): void {
this._inner.reset()
this._decodeBuf.reset()
this._encryptor?.close?.()
this._decryptor?.close?.()

View file

@ -6,15 +6,18 @@ export interface BasicDcOption {
id: number
ipv6?: boolean
mediaOnly?: boolean
testMode?: boolean
}
export function serializeBasicDcOption(dc: BasicDcOption): Uint8Array {
const writer = TlBinaryWriter.manual(64)
const flags = (dc.ipv6 ? 1 : 0) | (dc.mediaOnly ? 2 : 0)
const flags = (dc.ipv6 ? 1 : 0)
| (dc.mediaOnly ? 2 : 0)
| (dc.testMode ? 4 : 0)
writer.raw(
new Uint8Array([
1, // version
2, // version
dc.id,
flags,
]),
@ -30,7 +33,7 @@ export function parseBasicDcOption(data: Uint8Array): BasicDcOption | null {
const reader = TlBinaryReader.manual(data)
const [version, id, flags] = reader.raw(3)
if (version !== 1) return null
if (version !== 1 && version !== 2) return null
const ipAddress = reader.string()
const port = reader.int()
@ -41,6 +44,7 @@ export function parseBasicDcOption(data: Uint8Array): BasicDcOption | null {
port,
ipv6: (flags & 1) !== 0,
mediaOnly: (flags & 2) !== 0,
testMode: version === 2 && (flags & 4) !== 0,
}
}
@ -84,12 +88,14 @@ export const defaultTestDc: DcOptions = {
ipAddress: '149.154.167.40',
port: 443,
id: 2,
testMode: true,
},
media: {
ipAddress: '149.154.167.40',
port: 443,
id: 2,
mediaOnly: true,
testMode: true,
},
}
@ -99,6 +105,7 @@ export const defaultTestIpv6Dc: DcOptions = {
port: 443,
ipv6: true,
id: 2,
testMode: true,
},
media: {
ipAddress: '2001:67c:4e8:f002::e',
@ -106,5 +113,6 @@ export const defaultTestIpv6Dc: DcOptions = {
ipv6: true,
id: 2,
mediaOnly: true,
testMode: true,
},
}

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/create-bot",
"type": "module",
"version": "0.16.13",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Bot starter kit for mtcute",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/crypto-node",
"type": "module",
"version": "0.16.9",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Native crypto implementation for NodeJS",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/deno",
"type": "module",
"version": "0.16.9",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Meta-package for Deno",
"author": "alina sireneva <alina@tei.su>",
@ -19,6 +19,9 @@
},
"dependencies": {
"@db/sqlite": "npm:@jsr/db__sqlite@0.12.0",
"@fuman/deno-net": "workspace:^",
"@fuman/net": "workspace:^",
"@fuman/io": "workspace:^",
"@mtcute/core": "workspace:^",
"@mtcute/html-parser": "workspace:^",
"@mtcute/markdown-parser": "workspace:^",

View file

@ -2,7 +2,8 @@ export * from './client.js'
export * from './platform.js'
export * from './sqlite/index.js'
export * from './utils/crypto.js'
// export * from './utils/tcp.js'
export * from './utils/tcp.js'
export * from './utils/proxies.js'
export * from './worker.js'
export * from '@mtcute/core'
export * from '@mtcute/html-parser'

View file

@ -0,0 +1,74 @@
import type { HttpProxySettings as FumanHttpProxySettings, ITcpConnection, SocksProxySettings, TcpEndpoint } from '@fuman/net'
import { performHttpProxyHandshake, performSocksHandshake } from '@fuman/net'
import { connectTcp, connectTls } from '@fuman/deno-net'
import { BaseMtProxyTransport, type ITelegramConnection, IntermediatePacketCodec, type TelegramTransport } from '@mtcute/core'
import type { BasicDcOption } from '@mtcute/core/utils.js'
export type { SocksProxySettings } from '@fuman/net'
export { SocksProxyConnectionError, HttpProxyConnectionError } from '@fuman/net'
export type { MtProxySettings } from '@mtcute/core'
export interface HttpProxySettings extends FumanHttpProxySettings {
/**
* Whether this is a HTTPS proxy (by default it is regular HTTP).
*/
tls?: boolean
}
export class HttpProxyTcpTransport implements TelegramTransport {
constructor(readonly proxy: HttpProxySettings) {}
async connect(dc: BasicDcOption): Promise<ITelegramConnection> {
let conn
if (this.proxy.tls) {
conn = await connectTls({
address: this.proxy.host,
port: this.proxy.port,
})
} else {
conn = await connectTcp({
address: this.proxy.host,
port: this.proxy.port,
})
}
await performHttpProxyHandshake(conn, conn, this.proxy, {
address: dc.ipAddress,
port: dc.port,
})
return conn
}
packetCodec(): IntermediatePacketCodec {
return new IntermediatePacketCodec()
}
}
export class SocksProxyTcpTransport implements TelegramTransport {
constructor(readonly proxy: SocksProxySettings) {}
async connect(dc: BasicDcOption): Promise<ITelegramConnection> {
const conn = await connectTcp({
address: this.proxy.host,
port: this.proxy.port,
})
await performSocksHandshake(conn, conn, this.proxy, {
address: dc.ipAddress,
port: dc.port,
})
return conn
}
packetCodec(): IntermediatePacketCodec {
return new IntermediatePacketCodec()
}
}
export class MtProxyTcpTransport extends BaseMtProxyTransport {
async _connectTcp(endpoint: TcpEndpoint): Promise<ITcpConnection> {
return connectTcp(endpoint)
}
}

View file

@ -1,146 +1,7 @@
// import EventEmitter from 'node:events'
import { connectTcp } from '@fuman/deno-net'
import { IntermediatePacketCodec, type TelegramTransport } from '@mtcute/core'
// import { IntermediatePacketCodec, IPacketCodec, ITelegramTransport, MtcuteError, TransportState } from '@mtcute/core'
// import { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js'
// import { writeAll } from '@std/io/write-all'
// /**
// * Base for TCP transports.
// * Subclasses must provide packet codec in `_packetCodec` property
// */
// export abstract class BaseTcpTransport extends EventEmitter implements ITelegramConnection {
// protected _currentDc: BasicDcOption | null = null
// protected _state: TransportState = TransportState.Idle
// protected _socket: Deno.TcpConn | null = null
// abstract _packetCodec: IPacketCodec
// protected _crypto!: ICryptoProvider
// protected log!: Logger
// packetCodecInitialized = false
// private _updateLogPrefix() {
// if (this._currentDc) {
// this.log.prefix = `[TCP:${this._currentDc.ipAddress}:${this._currentDc.port}] `
// } else {
// this.log.prefix = '[TCP:disconnected] '
// }
// }
// setup(crypto: ICryptoProvider, log: Logger): void {
// this._crypto = crypto
// this.log = log.create('tcp')
// this._updateLogPrefix()
// }
// state(): TransportState {
// return this._state
// }
// currentDc(): BasicDcOption | null {
// return this._currentDc
// }
// // eslint-disable-next-line unused-imports/no-unused-vars
// connect(dc: BasicDcOption, testMode: boolean): void {
// if (this._state !== TransportState.Idle) {
// throw new MtcuteError('Transport is not IDLE')
// }
// if (!this.packetCodecInitialized) {
// 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.packetCodecInitialized = true
// }
// this._state = TransportState.Connecting
// this._currentDc = dc
// this._updateLogPrefix()
// this.log.debug('connecting to %j', dc)
// Deno.connect({
// hostname: dc.ipAddress,
// port: dc.port,
// transport: 'tcp',
// })
// .then(this.handleConnect.bind(this))
// .catch((err) => {
// this.handleError(err)
// this.close()
// })
// }
// close(): void {
// if (this._state === TransportState.Idle) return
// this.log.info('connection closed')
// this._state = TransportState.Idle
// try {
// this._socket?.close()
// } catch (e) {
// if (!(e instanceof Deno.errors.BadResource)) {
// this.handleError(e)
// }
// }
// this._socket = null
// this._currentDc = null
// this._packetCodec.reset()
// this.emit('close')
// }
// handleError(error: unknown): void {
// this.log.error('error: %s', error)
// if (this.listenerCount('error') > 0) {
// this.emit('error', error)
// }
// }
// async handleConnect(socket: Deno.TcpConn): Promise<void> {
// this._socket = socket
// this.log.info('connected')
// try {
// const packet = await this._packetCodec.tag()
// if (packet.length) {
// await writeAll(this._socket, packet)
// }
// this._state = TransportState.Ready
// this.emit('ready')
// const reader = this._socket.readable.getReader()
// while (true) {
// const { done, value } = await reader.read()
// if (done) break
// this._packetCodec.feed(value)
// }
// } catch (e) {
// this.handleError(e)
// }
// this.close()
// }
// async send(bytes: Uint8Array): Promise<void> {
// const framed = await this._packetCodec.encode(bytes)
// if (this._state !== TransportState.Ready) {
// throw new MtcuteError('Transport is not READY')
// }
// await writeAll(this._socket!, framed)
// }
// }
// export class TcpTransport extends BaseTcpTransport {
// _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec()
// }
export const TcpTransport: TelegramTransport = {
connect: dc => connectTcp({ address: dc.ipAddress, port: dc.port }),
packetCodec: () => new IntermediatePacketCodec(),
}

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/dispatcher",
"type": "module",
"version": "0.16.13",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Updates dispatcher and bot framework for @mtcute/client",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/file-id",
"type": "module",
"version": "0.16.12",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Support for TDLib and Bot API file ID for mtcute",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/html-parser",
"type": "module",
"version": "0.16.9",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "HTML entities parser for mtcute",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,19 +0,0 @@
# @mtcute/http-proxy
📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_http_proxy.html)
HTTP(s) proxy transport for mtcute.
## Usage
```typescript
import { HttpProxyTcpTransport } from '@mtcute/socks-proxy'
const tg = new TelegramClient({
// ...
transport: () => new HttpProxyTcpTransport({
host: 'localhost',
port: 1080,
})
})
```

View file

@ -1,165 +0,0 @@
// todo: move to fuman
// import { connect as connectTcp } from 'node:net'
// import type { SecureContextOptions } from 'node:tls'
// import { connect as connectTls } from 'node:tls'
// import type { tl } from '@mtcute/node'
// import { BaseTcpTransport, IntermediatePacketCodec, MtcuteError, NodePlatform, TransportState } from '@mtcute/node'
// /**
// * An error has occurred while connecting to an HTTP(s) proxy
// */
// export class HttpProxyConnectionError extends Error {
// readonly proxy: HttpProxySettings
// constructor(proxy: HttpProxySettings, message: string) {
// super(`Error while connecting to ${proxy.host}:${proxy.port}: ${message}`)
// this.proxy = proxy
// }
// }
// /**
// * HTTP(s) proxy settings
// */
// export interface HttpProxySettings {
// /**
// * 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
// /**
// * 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 BaseHttpProxyTcpTransport extends BaseTcpTransport {
// readonly _proxy: HttpProxySettings
// constructor(proxy: HttpProxySettings) {
// super()
// this._proxy = proxy
// }
// private _platform = new NodePlatform()
// connect(dc: tl.RawDcOption): void {
// if (this._state !== TransportState.Idle) {
// throw new MtcuteError('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() {
// this.log.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 ${this._platform.base64Encode(this._platform.utf8Encode(auth))}`
// }
// headers['Proxy-Connection'] = 'Keep-Alive'
// const headersStr = Object.keys(headers)
// .map(k => `\r\n${k}: ${headers[k]}`)
// .join('')
// const packet = `CONNECT ${ip} HTTP/1.1${headersStr}\r\n\r\n`
// this._socket!.write(packet)
// this._socket!.once('data', (msg) => {
// this.log.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(this._proxy, `Server returned invalid protocol: ${proto}`),
// )
// return
// }
// if (code[0] !== '2') {
// this._socket!.emit(
// 'error',
// new HttpProxyConnectionError(this._proxy, `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()
// })
// }
// }
// /**
// * HTTP(s) TCP transport using an intermediate packet codec.
// *
// * Should be the one passed as `transport` to `TelegramClient` constructor
// * (unless you want to use a custom codec).
// */
// export class HttpProxyTcpTransport extends BaseHttpProxyTcpTransport {
// _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec()
// }

View file

@ -1,18 +0,0 @@
{
"name": "@mtcute/http-proxy",
"type": "module",
"version": "0.16.9",
"private": true,
"description": "HTTP(S) proxy support for mtcute",
"author": "alina sireneva <alina@tei.su>",
"license": "MIT",
"sideEffects": false,
"exports": "./index.ts",
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package http-proxy"
},
"dependencies": {
"@mtcute/node": "workspace:^"
}
}

View file

@ -1,6 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": [
"./index.ts"
]
}

View file

@ -1,4 +0,0 @@
module.exports = {
extends: ['../../.config/typedoc/config.base.cjs'],
entryPoints: ['./index.ts'],
}

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/i18n",
"type": "module",
"version": "0.16.9",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "I18n for mtcute",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/markdown-parser",
"type": "module",
"version": "0.16.9",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Markdown entities parser for mtcute",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,22 +0,0 @@
# @mtcute/mtproxy
📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_mtproxy.html)
MTProto proxy (MTProxy) transport for mtcute.
Supports all kinds of MTProto proxies, including obfuscated and fake TLS.
## Usage
```typescript
import { MtProxyTcpTransport } from '@mtcute/mtproxy'
const tg = new TelegramClient({
// ...
transport: () => new MtProxyTcpTransport({
host: 'localhost',
port: 443,
secret: 'secret'
})
})
```

View file

@ -1,352 +0,0 @@
// /* eslint-disable no-restricted-globals */
// todo fixme
// import type { IPacketCodec } from '@mtcute/node'
// import { WrappedCodec } from '@mtcute/node'
// import type { ICryptoProvider } from '@mtcute/node/utils.js'
// import { bigIntModInv, bigIntModPow, bigIntToBuffer, bufferToBigInt } from '@mtcute/node/utils.js'
// const MAX_TLS_PACKET_LENGTH = 2878
// const TLS_FIRST_PREFIX = Buffer.from('140303000101', 'hex')
// // ref: https://github.com/tdlib/td/blob/master/td/mtproto/TlsInit.cpp
// const KEY_MOD = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEDn
// // 2^255 - 19
// const QUAD_RES_MOD = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEDn
// // (mod - 1) / 2 = 2^254 - 10
// const QUAD_RES_POW = 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6n
// function _getY2(x: bigint, mod: bigint): bigint {
// // returns y = x^3 + x^2 * 486662 + x
// let y = x
// y = (y + 486662n) % mod
// y = (y * x) % mod
// y = (y + 1n) % mod
// y = (y * x) % mod
// return y
// }
// function _getDoubleX(x: bigint, mod: bigint): bigint {
// // returns x_2 = (x^2 - 1)^2/(4*y^2)
// let denominator = _getY2(x, mod)
// denominator = (denominator * 4n) % mod
// let numerator = (x * x) % mod
// numerator = (numerator - 1n) % mod
// numerator = (numerator * numerator) % mod
// denominator = bigIntModInv(denominator, mod)
// numerator = (numerator * denominator) % mod
// return numerator
// }
// function _isQuadraticResidue(a: bigint): boolean {
// const r = bigIntModPow(a, QUAD_RES_POW, QUAD_RES_MOD)
// return r === 1n
// }
// interface TlsOperationHandler {
// string: (buf: Buffer) => void
// zero: (size: number) => void
// random: (size: number) => void
// domain: () => void
// grease: (seed: number) => void
// beginScope: () => void
// endScope: () => void
// key: () => void
// }
// function executeTlsOperations(h: TlsOperationHandler): void {
// h.string(Buffer.from('1603010200010001fc0303', 'hex'))
// h.zero(32)
// h.string(Buffer.from('20', 'hex'))
// h.random(32)
// h.string(Buffer.from('0020', 'hex'))
// h.grease(0)
// h.string(Buffer.from('130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f003501000193', 'hex'))
// h.grease(2)
// h.string(Buffer.from('00000000', 'hex'))
// h.beginScope()
// h.beginScope()
// h.string(Buffer.from('00', 'hex'))
// h.beginScope()
// h.domain()
// h.endScope()
// h.endScope()
// h.endScope()
// h.string(Buffer.from('00170000ff01000100000a000a0008', 'hex'))
// h.grease(4)
// h.string(
// Buffer.from(
// '001d00170018000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0012001004030804040105030805050108060601001200000033002b0029',
// 'hex',
// ),
// )
// h.grease(4)
// h.string(Buffer.from('000100001d0020', 'hex'))
// h.key()
// h.string(Buffer.from('002d00020101002b000b0a', 'hex'))
// h.grease(6)
// h.string(Buffer.from('0304030303020301001b0003020002', 'hex'))
// h.grease(3)
// h.string(Buffer.from('0001000015', 'hex'))
// }
// // i dont know why is this needed, since it is always padded to 517 bytes
// // this was in tdlib sources, so whatever. not used here though, and works just fine
// // class TlsHelloCounter implements TlsOperationHandler {
// // size = 0
// //
// // private _domain: Buffer
// //
// // constructor(domain: Buffer) {
// // this._domain = domain
// // }
// //
// // string(buf: Buffer) {
// // this.size += buf.length
// // }
// //
// // random(size: number) {
// // this.size += size
// // }
// //
// // zero(size: number) {
// // this.size += size
// // }
// //
// // domain() {
// // this.size += this._domain.length
// // }
// //
// // grease() {
// // this.size += 2
// // }
// //
// // key() {
// // this.size += 32
// // }
// //
// // beginScope() {
// // this.size += 2
// // }
// //
// // endScope() {
// // // no-op, since this does not affect size
// // }
// //
// // finish(): number {
// // const zeroPad = 515 - this.size
// // this.beginScope()
// // this.zero(zeroPad)
// // this.endScope()
// //
// // return this.size
// // }
// // }
// function initGrease(crypto: ICryptoProvider, size: number): Buffer {
// const buf = crypto.randomBytes(size)
// for (let i = 0; i < size; i++) {
// buf[i] = (buf[i] & 0xF0) + 0x0A
// }
// for (let i = 1; i < size; i += 2) {
// if (buf[i] === buf[i - 1]) {
// buf[i] ^= 0x10
// }
// }
// return Buffer.from(buf)
// }
// class TlsHelloWriter implements TlsOperationHandler {
// buf: Buffer
// pos = 0
// private _domain: Buffer
// private _grease
// private _scopes: number[] = []
// constructor(
// readonly crypto: ICryptoProvider,
// size: number,
// domain: Buffer,
// ) {
// this._domain = domain
// this.buf = Buffer.allocUnsafe(size)
// this._grease = initGrease(this.crypto, 7)
// }
// string(buf: Buffer) {
// buf.copy(this.buf, this.pos)
// this.pos += buf.length
// }
// random(size: number) {
// this.string(Buffer.from(this.crypto.randomBytes(size)))
// }
// zero(size: number) {
// this.string(Buffer.alloc(size, 0))
// }
// domain() {
// this.string(this._domain)
// }
// grease(seed: number) {
// this.buf[this.pos] = this.buf[this.pos + 1] = this._grease[seed]
// this.pos += 2
// }
// key() {
// for (;;) {
// const key = this.crypto.randomBytes(32)
// key[31] &= 127
// let x = bufferToBigInt(key)
// const y = _getY2(x, KEY_MOD)
// if (_isQuadraticResidue(y)) {
// for (let i = 0; i < 3; i++) {
// x = _getDoubleX(x, KEY_MOD)
// }
// const key = bigIntToBuffer(x, 32, true)
// this.string(Buffer.from(key))
// return
// }
// }
// }
// beginScope() {
// this._scopes.push(this.pos)
// this.pos += 2
// }
// endScope() {
// const begin = this._scopes.pop()
// if (begin === undefined) {
// throw new Error('endScope called without beginScope')
// }
// const end = this.pos
// const size = end - begin - 2
// this.buf.writeUInt16BE(size, begin)
// }
// async finish(secret: Buffer): Promise<Buffer> {
// const padSize = 515 - this.pos
// const unixTime = ~~(Date.now() / 1000)
// this.beginScope()
// this.zero(padSize)
// this.endScope()
// const hash = Buffer.from(await this.crypto.hmacSha256(this.buf, secret))
// const old = hash.readInt32LE(28)
// hash.writeInt32LE(old ^ unixTime, 28)
// hash.copy(this.buf, 11)
// return this.buf
// }
// }
// /** @internal */
// export async function generateFakeTlsHeader(domain: string, secret: Buffer, crypto: ICryptoProvider): Promise<Buffer> {
// const domainBuf = Buffer.from(domain)
// const writer = new TlsHelloWriter(crypto, 517, domainBuf)
// executeTlsOperations(writer)
// return writer.finish(secret)
// }
// /**
// * Fake TLS packet codec, used for some MTProxies.
// *
// * Must only be used inside {@link MtProxyTcpTransport}
// * @internal
// */
// export class FakeTlsPacketCodec extends WrappedCodec implements IPacketCodec {
// protected _stream: Buffer = Buffer.alloc(0)
// private _header!: Buffer
// private _isFirstTls = true
// async tag(): Promise<Buffer> {
// this._header = Buffer.from(await this._inner.tag())
// return Buffer.alloc(0)
// }
// private _encodeTls(packet: Buffer): Buffer {
// if (this._header.length) {
// packet = Buffer.concat([this._header, packet])
// this._header = Buffer.alloc(0)
// }
// const header = Buffer.from([0x17, 0x03, 0x03, 0x00, 0x00])
// header.writeUInt16BE(packet.length, 3)
// if (this._isFirstTls) {
// this._isFirstTls = false
// return Buffer.concat([TLS_FIRST_PREFIX, header, packet])
// }
// return Buffer.concat([header, packet])
// }
// async encode(packet: Buffer): Promise<Buffer> {
// packet = Buffer.from(await this._inner.encode(packet))
// if (packet.length + this._header.length > MAX_TLS_PACKET_LENGTH) {
// const ret: Buffer[] = []
// while (packet.length) {
// const buf = packet.slice(0, MAX_TLS_PACKET_LENGTH - this._header.length)
// packet = packet.slice(buf.length)
// ret.push(this._encodeTls(buf))
// }
// return Buffer.concat(ret)
// }
// return this._encodeTls(packet)
// }
// feed(data: Buffer): void {
// this._stream = Buffer.concat([this._stream, data])
// for (;;) {
// if (this._stream.length < 5) return
// if (!(this._stream[0] === 0x17 && this._stream[1] === 0x03 && this._stream[2] === 0x03)) {
// this.emit('error', new Error('Invalid TLS header'))
// return
// }
// const length = this._stream.readUInt16BE(3)
// if (length < this._stream.length - 5) return
// this._inner.feed(this._stream.slice(5, length + 5))
// this._stream = this._stream.slice(length + 5)
// }
// }
// reset(): void {
// this._stream = Buffer.alloc(0)
// this._isFirstTls = true
// }
// }

View file

@ -1,250 +0,0 @@
// /* eslint-disable no-restricted-globals */
// todo fixme
// import { connect } from 'node:net'
// import type {
// IPacketCodec,
// tl,
// } from '@mtcute/node'
// import {
// BaseTcpTransport,
// IntermediatePacketCodec,
// MtSecurityError,
// MtUnsupportedError,
// MtcuteError,
// ObfuscatedPacketCodec,
// PaddedIntermediatePacketCodec,
// TransportState,
// } from '@mtcute/node'
// import { buffersEqual } from '@mtcute/node/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 ts/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)
// }
// }
// }

View file

@ -1,18 +0,0 @@
{
"name": "@mtcute/mtproxy",
"type": "module",
"version": "0.16.9",
"private": true,
"description": "MTProto proxy (MTProxy) support for mtcute",
"author": "alina sireneva <alina@tei.su>",
"license": "MIT",
"sideEffects": false,
"exports": "./index.ts",
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package mtproxy"
},
"dependencies": {
"@mtcute/node": "workspace:^"
}
}

View file

@ -1,7 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": [
"./index.ts",
"./fake-tls.ts"
]
}

View file

@ -1,4 +0,0 @@
module.exports = {
extends: ['../../.config/typedoc/config.base.cjs'],
entryPoints: ['./index.ts'],
}

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/node",
"type": "module",
"version": "0.16.13",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Meta-package for Node.js",
"author": "alina sireneva <alina@tei.su>",
@ -21,6 +21,7 @@
"@mtcute/html-parser": "workspace:^",
"@mtcute/markdown-parser": "workspace:^",
"@mtcute/wasm": "workspace:^",
"@fuman/net": "workspace:^",
"@fuman/node-net": "workspace:^",
"better-sqlite3": "11.3.0"
},

View file

@ -2,6 +2,7 @@ export * from './client.js'
export * from './common-internals-node/platform.js'
export * from './sqlite/index.js'
export * from './utils/tcp.js'
export * from './utils/proxies.js'
export * from './worker.js'
export * from '@mtcute/core'
export * from '@mtcute/html-parser'

View file

@ -0,0 +1,84 @@
import type { SecureContextOptions } from 'node:tls'
import type { HttpProxySettings as FumanHttpProxySettings, ITcpConnection, SocksProxySettings, TcpEndpoint } from '@fuman/net'
import { performHttpProxyHandshake, performSocksHandshake } from '@fuman/net'
import { connectTcp, connectTls } from '@fuman/node-net'
import { BaseMtProxyTransport, type ITelegramConnection, IntermediatePacketCodec, type TelegramTransport } from '@mtcute/core'
import type { BasicDcOption } from '@mtcute/core/utils.js'
export type { SocksProxySettings } from '@fuman/net'
export { SocksProxyConnectionError, HttpProxyConnectionError } from '@fuman/net'
export type { MtProxySettings } from '@mtcute/core'
export interface HttpProxySettings extends FumanHttpProxySettings {
/**
* Whether this is a HTTPS proxy (by default it is regular HTTP).
*/
tls?: boolean
/**
* Additional TLS options, used if `tls = true`.
* Can contain stuff like custom certificate, host,
* or whatever.
*/
tlsOptions?: SecureContextOptions
}
export class HttpProxyTcpTransport implements TelegramTransport {
constructor(readonly proxy: HttpProxySettings) {}
async connect(dc: BasicDcOption): Promise<ITelegramConnection> {
let conn
if (this.proxy.tls) {
conn = await connectTls({
address: this.proxy.host,
port: this.proxy.port,
extraOptions: this.proxy.tlsOptions,
})
} else {
conn = await connectTcp({
address: this.proxy.host,
port: this.proxy.port,
})
}
await performHttpProxyHandshake(conn, conn, this.proxy, {
address: dc.ipAddress,
port: dc.port,
})
return conn
}
packetCodec(): IntermediatePacketCodec {
return new IntermediatePacketCodec()
}
}
export class SocksProxyTcpTransport implements TelegramTransport {
constructor(readonly proxy: SocksProxySettings) {}
async connect(dc: BasicDcOption): Promise<ITelegramConnection> {
const conn = await connectTcp({
address: this.proxy.host,
port: this.proxy.port,
})
await performSocksHandshake(conn, conn, this.proxy, {
address: dc.ipAddress,
port: dc.port,
})
return conn
}
packetCodec(): IntermediatePacketCodec {
return new IntermediatePacketCodec()
}
}
export class MtProxyTcpTransport extends BaseMtProxyTransport {
async _connectTcp(endpoint: TcpEndpoint): Promise<ITcpConnection> {
return connectTcp(endpoint)
}
}

View file

@ -1,19 +0,0 @@
# @mtcute/socks-proxy
📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_socks_proxy.html)
Socks4/5 proxy transport for mtcute.
## Usage
```typescript
import { SocksTcpTransport } from '@mtcute/socks-proxy'
const tg = new TelegramClient({
// ...
transport: () => new SocksTcpTransport({
host: 'localhost',
port: 1080,
})
})
```

View file

@ -1,420 +0,0 @@
// todo: move to fuman
// import { connect } from 'node:net'
// // @ts-expect-error no typings
// import { normalize } from 'ip6'
// import type { tl } from '@mtcute/node'
// import { BaseTcpTransport, IntermediatePacketCodec, MtArgumentError, NodePlatform, TransportState, assertNever } from '@mtcute/node'
// import { dataViewFromBuffer } from '@mtcute/node/utils.js'
// const p = new NodePlatform()
// /**
// * An error has occurred while connecting to an SOCKS proxy
// */
// export class SocksProxyConnectionError extends Error {
// readonly proxy: SocksProxySettings
// constructor(proxy: SocksProxySettings, message: string) {
// super(`Error while connecting to ${proxy.host}:${proxy.port}: ${message}`)
// this.proxy = proxy
// }
// }
// /**
// * Settings for a SOCKS4/5 proxy
// */
// export interface SocksProxySettings {
// /**
// * 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
// /**
// * Proxy authorization username, if needed
// */
// user?: string
// /**
// * Proxy authorization password, if needed
// */
// password?: string
// /**
// * Version of the SOCKS proxy (4 or 5)
// *
// * @default `5`
// */
// version?: 4 | 5
// }
// function writeIpv4(ip: string, buf: Uint8Array, offset: number): void {
// const parts = ip.split('.')
// if (parts.length !== 4) {
// throw new MtArgumentError('Invalid IPv4 address')
// }
// for (let i = 0; i < 4; i++) {
// const n = Number.parseInt(parts[i])
// if (Number.isNaN(n) || n < 0 || n > 255) {
// throw new MtArgumentError('Invalid IPv4 address')
// }
// buf[offset + i] = n
// }
// }
// function buildSocks4ConnectRequest(ip: string, port: number, username = ''): Uint8Array {
// const userId = p.utf8Encode(username)
// const buf = new Uint8Array(9 + userId.length)
// buf[0] = 0x04 // VER
// buf[1] = 0x01 // CMD = establish a TCP/IP stream connection
// dataViewFromBuffer(buf).setUint16(2, port, false)
// writeIpv4(ip, buf, 4) // DSTIP
// buf.set(userId, 8)
// buf[8 + userId.length] = 0x00 // ID (null-termination)
// return buf
// }
// function buildSocks5Greeting(authAvailable: boolean): Uint8Array {
// const buf = new Uint8Array(authAvailable ? 4 : 3)
// buf[0] = 0x05 // VER
// if (authAvailable) {
// buf[1] = 0x02 // NAUTH
// buf[2] = 0x00 // AUTH[0] = No authentication
// buf[3] = 0x02 // AUTH[1] = Username/password
// } else {
// buf[1] = 0x01 // NAUTH
// buf[2] = 0x00 // AUTH[0] = No authentication
// }
// return buf
// }
// function buildSocks5Auth(username: string, password: string) {
// const usernameBuf = p.utf8Encode(username)
// const passwordBuf = p.utf8Encode(password)
// if (usernameBuf.length > 255) {
// throw new MtArgumentError(`Too long username (${usernameBuf.length} > 255)`)
// }
// if (passwordBuf.length > 255) {
// throw new MtArgumentError(`Too long password (${passwordBuf.length} > 255)`)
// }
// const buf = new Uint8Array(3 + usernameBuf.length + passwordBuf.length)
// buf[0] = 0x01 // VER of auth
// buf[1] = usernameBuf.length
// buf.set(usernameBuf, 2)
// buf[2 + usernameBuf.length] = passwordBuf.length
// buf.set(passwordBuf, 3 + usernameBuf.length)
// return buf
// }
// function writeIpv6(ip: string, buf: Uint8Array, offset: number): void {
// // eslint-disable-next-line ts/no-unsafe-call
// ip = normalize(ip) as string
// const parts = ip.split(':')
// if (parts.length !== 8) {
// throw new MtArgumentError('Invalid IPv6 address')
// }
// const dv = dataViewFromBuffer(buf)
// for (let i = 0, j = offset; i < 8; i++, j += 2) {
// const n = Number.parseInt(parts[i])
// if (Number.isNaN(n) || n < 0 || n > 0xFFFF) {
// throw new MtArgumentError('Invalid IPv6 address')
// }
// dv.setUint16(j, n, false)
// }
// }
// function buildSocks5Connect(ip: string, port: number, ipv6 = false): Uint8Array {
// const buf = new Uint8Array(ipv6 ? 22 : 10)
// const dv = dataViewFromBuffer(buf)
// buf[0] = 0x05 // VER
// buf[1] = 0x01 // CMD = establish a TCP/IP stream connection
// buf[2] = 0x00 // RSV
// if (ipv6) {
// buf[3] = 0x04 // TYPE = IPv6
// writeIpv6(ip, buf, 4) // ADDR
// dv.setUint16(20, port, false)
// } else {
// buf[3] = 0x01 // TYPE = IPv4
// writeIpv4(ip, buf, 4) // ADDR
// dv.setUint16(8, port, false)
// }
// return buf
// }
// const SOCKS4_ERRORS: Record<number, string> = {
// 91: 'Request rejected or failed',
// 92: 'Request failed because client is not running identd',
// 93: "Request failed because client's identd could not confirm the user ID in the request",
// }
// const SOCKS5_ERRORS: Record<number, string> = {
// 1: 'General failure',
// 2: 'Connection not allowed by ruleset',
// 3: 'Network unreachable',
// 4: 'Host unreachable',
// 5: 'Connection refused by destination host',
// 6: 'TTL expired',
// 7: 'Command not supported / protocol error',
// 8: 'Address type not supported',
// }
// /**
// * TCP transport that connects via a SOCKS4/5 proxy.
// */
// export abstract class BaseSocksTcpTransport extends BaseTcpTransport {
// readonly _proxy: SocksProxySettings
// constructor(proxy: SocksProxySettings) {
// super()
// if (proxy.version != null && proxy.version !== 4 && proxy.version !== 5) {
// throw new SocksProxyConnectionError(
// proxy,
// `Invalid SOCKS version: ${proxy.version}`,
// )
// }
// this._proxy = proxy
// }
// connect(dc: tl.RawDcOption): void {
// if (this._state !== TransportState.Idle) {
// throw new MtArgumentError('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 = connect(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() {
// let packetHandler: (msg: Uint8Array) => void
// if (this._proxy.version === 4) {
// packetHandler = (msg) => {
// if (msg[0] !== 0x04) {
// // VER, must be 4
// this._socket!.emit(
// 'error',
// new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`),
// )
// return
// }
// const code = msg[1]
// this.log.debug('[%s:%d] CONNECT returned code %d', this._proxy.host, this._proxy.port, code)
// if (code === 0x5A) {
// this._socket!.off('data', packetHandler)
// this._socket!.on('data', data => this._packetCodec.feed(data))
// this.handleConnect()
// } else {
// const msg
// = code in SOCKS4_ERRORS ? SOCKS4_ERRORS[code] : `Unknown error code: 0x${code.toString(16)}`
// this._socket!.emit('error', new SocksProxyConnectionError(this._proxy, msg))
// }
// }
// this.log.debug('[%s:%d] connected to proxy, sending CONNECT', this._proxy.host, this._proxy.port)
// try {
// this._socket!.write(
// buildSocks4ConnectRequest(this._currentDc!.ipAddress, this._currentDc!.port, this._proxy.user),
// )
// } catch (e) {
// this._socket!.emit('error', e)
// }
// } else {
// let state: 'greeting' | 'auth' | 'connect' = 'greeting'
// const sendConnect = () => {
// this.log.debug('[%s:%d] sending CONNECT', this._proxy.host, this._proxy.port)
// try {
// this._socket!.write(
// buildSocks5Connect(this._currentDc!.ipAddress, this._currentDc!.port, this._currentDc!.ipv6),
// )
// state = 'connect'
// } catch (e) {
// this._socket!.emit('error', e)
// }
// }
// packetHandler = (msg) => {
// switch (state) {
// case 'greeting': {
// if (msg[0] !== 0x05) {
// // VER, must be 5
// this._socket!.emit(
// 'error',
// new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`),
// )
// return
// }
// const chosen = msg[1]
// this.log.debug(
// '[%s:%d] GREETING returned auth method %d',
// this._proxy.host,
// this._proxy.port,
// chosen,
// )
// switch (chosen) {
// case 0x00:
// // "No authentication"
// sendConnect()
// break
// case 0x02:
// // Username/password
// if (!this._proxy.user || !this._proxy.password) {
// // should not happen
// this._socket!.emit(
// 'error',
// new SocksProxyConnectionError(
// this._proxy,
// 'Authentication is required, but not provided',
// ),
// )
// break
// }
// try {
// this._socket!.write(buildSocks5Auth(this._proxy.user, this._proxy.password))
// state = 'auth'
// } catch (e) {
// this._socket!.emit('error', e)
// }
// break
// case 0xFF:
// default:
// // "no acceptable methods were offered"
// this._socket!.emit(
// 'error',
// new SocksProxyConnectionError(
// this._proxy,
// 'Authentication is required, but not provided/supported',
// ),
// )
// break
// }
// break
// }
// case 'auth':
// if (msg[0] !== 0x01) {
// // VER of auth, must be 1
// this._socket!.emit(
// 'error',
// new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`),
// )
// return
// }
// this.log.debug('[%s:%d] AUTH returned code %d', this._proxy.host, this._proxy.port, msg[1])
// if (msg[1] === 0x00) {
// // success
// sendConnect()
// } else {
// this._socket!.emit(
// 'error',
// new SocksProxyConnectionError(this._proxy, 'Authentication failure'),
// )
// }
// break
// case 'connect': {
// if (msg[0] !== 0x05) {
// // VER, must be 5
// this._socket!.emit(
// 'error',
// new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`),
// )
// return
// }
// const code = msg[1]
// this.log.debug('[%s:%d] CONNECT returned code %d', this._proxy.host, this._proxy.port, code)
// if (code === 0x00) {
// // Request granted
// this._socket!.off('data', packetHandler)
// this._socket!.on('data', data => this._packetCodec.feed(data))
// this.handleConnect()
// } else {
// const msg
// = code in SOCKS5_ERRORS
// ? SOCKS5_ERRORS[code]
// : `Unknown error code: 0x${code.toString(16)}`
// this._socket!.emit('error', new SocksProxyConnectionError(this._proxy, msg))
// }
// break
// }
// default:
// assertNever(state)
// }
// }
// this.log.debug('[%s:%d] connected to proxy, sending GREETING', this._proxy.host, this._proxy.port)
// try {
// this._socket!.write(buildSocks5Greeting(Boolean(this._proxy.user && this._proxy.password)))
// } catch (e) {
// this._socket!.emit('error', e)
// }
// }
// this._socket!.on('data', packetHandler)
// }
// }
// /**
// * Socks TCP transport using an intermediate packet codec.
// *
// * Should be the one passed as `transport` to `TelegramClient` constructor
// * (unless you want to use a custom codec).
// */
// export class SocksTcpTransport extends BaseSocksTcpTransport {
// _packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec()
// }

View file

@ -1,19 +0,0 @@
{
"name": "@mtcute/socks-proxy",
"type": "module",
"version": "0.16.9",
"private": true,
"description": "SOCKS4/5 proxy support for mtcute",
"author": "alina sireneva <alina@tei.su>",
"license": "MIT",
"sideEffects": false,
"exports": "./index.ts",
"scripts": {
"docs": "typedoc",
"build": "pnpm run -w build-package socks-proxy"
},
"dependencies": {
"@mtcute/node": "workspace:^",
"ip6": "0.2.7"
}
}

View file

@ -1,6 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": [
"./index.ts"
]
}

View file

@ -1,4 +0,0 @@
module.exports = {
extends: ['../../.config/typedoc/config.base.cjs'],
entryPoints: ['./index.ts'],
}

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/test",
"type": "module",
"version": "0.16.13",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Test utilities for mtcute",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/tl-runtime",
"type": "module",
"version": "0.16.9",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Runtime for TL",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/tl-utils",
"type": "module",
"version": "0.16.9",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Utils for working with TL schema",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/wasm",
"type": "module",
"version": "0.16.11",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "WASM implementation of common algorithms used in Telegram",
"author": "alina sireneva <alina@tei.su>",

View file

@ -1,7 +1,7 @@
{
"name": "@mtcute/web",
"type": "module",
"version": "0.16.13",
"version": "0.17.0-fuman-alpha",
"private": true,
"description": "Meta-package for the web platform",
"author": "alina sireneva <alina@tei.su>",

View file

@ -101,6 +101,15 @@ importers:
packages/bun:
dependencies:
'@fuman/bun-net':
specifier: workspace:^
version: link:../../private/fuman/packages/bun-net
'@fuman/io':
specifier: workspace:^
version: link:../../private/fuman/packages/io
'@fuman/net':
specifier: workspace:^
version: link:../../private/fuman/packages/net
'@mtcute/core':
specifier: workspace:^
version: link:../core
@ -217,6 +226,15 @@ importers:
'@db/sqlite':
specifier: npm:@jsr/db__sqlite@0.12.0
version: '@jsr/db__sqlite@0.12.0'
'@fuman/deno-net':
specifier: workspace:^
version: link:../../private/fuman/packages/deno-net
'@fuman/io':
specifier: workspace:^
version: link:../../private/fuman/packages/io
'@fuman/net':
specifier: workspace:^
version: link:../../private/fuman/packages/net
'@mtcute/core':
specifier: workspace:^
version: link:../core
@ -275,12 +293,6 @@ importers:
specifier: 5.2.3
version: 5.2.3
packages/http-proxy:
dependencies:
'@mtcute/node':
specifier: workspace:^
version: link:../node
packages/i18n:
devDependencies:
'@mtcute/core':
@ -299,14 +311,11 @@ importers:
specifier: 5.2.3
version: 5.2.3
packages/mtproxy:
dependencies:
'@mtcute/node':
specifier: workspace:^
version: link:../node
packages/node:
dependencies:
'@fuman/net':
specifier: workspace:^
version: link:../../private/fuman/packages/net
'@fuman/node-net':
specifier: workspace:^
version: link:../../private/fuman/packages/node-net
@ -333,15 +342,6 @@ importers:
specifier: 7.6.4
version: 7.6.4
packages/socks-proxy:
dependencies:
'@mtcute/node':
specifier: workspace:^
version: link:../node
ip6:
specifier: 0.2.7
version: 0.2.7
packages/test:
dependencies:
'@mtcute/core':
@ -2778,10 +2778,6 @@ packages:
resolution: {integrity: sha512-B2LafrnnhbRzCWfAdOXisUzL89Kg8cVJlYmhqoi3flSiV/TveO+nsXwgKr9h9PIo+J1hz7nBSk6gegRIMBBf7g==}
engines: {node: '>=14.18.0'}
ip6@0.2.7:
resolution: {integrity: sha512-zEzGsxn4Uw33TByv0DdX/RRh+VsGfEctOp7CvJq/b4JEjY9OvPB58dsMYiEwIVLsIWHZSJPn3XG8mP9Qv3TG3g==}
hasBin: true
is-alphabetical@1.0.4:
resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==}
@ -6680,8 +6676,6 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
ip6@0.2.7: {}
is-alphabetical@1.0.4: {}
is-alphanumerical@1.0.4: