feat: mtproxy support
also some refactor in core transports
This commit is contained in:
parent
76f078d931
commit
2bd94782e9
17 changed files with 839 additions and 71 deletions
|
@ -9,7 +9,12 @@ export * from './utils/tl-json'
|
|||
export * from './utils/async-lock'
|
||||
export * from './utils/lru-map'
|
||||
export * from './utils/function-utils'
|
||||
export { encodeUrlSafeBase64, parseUrlSafeBase64 } from './utils/buffer-utils'
|
||||
export {
|
||||
encodeUrlSafeBase64,
|
||||
parseUrlSafeBase64,
|
||||
randomBytes,
|
||||
} from './utils/buffer-utils'
|
||||
export * from './utils/bigint-utils'
|
||||
|
||||
export { BinaryReader } from './utils/binary/binary-reader'
|
||||
export { BinaryWriter } from './utils/binary/binary-writer'
|
||||
|
|
|
@ -112,7 +112,7 @@ export class MtprotoSession {
|
|||
|
||||
const authKeyId = reader.raw(8)
|
||||
const messageKey = reader.int128()
|
||||
const encryptedData = reader.raw()
|
||||
let encryptedData = reader.raw()
|
||||
|
||||
if (!buffersEqual(authKeyId, this._authKeyId!)) {
|
||||
debug(
|
||||
|
@ -124,6 +124,13 @@ export class MtprotoSession {
|
|||
return null
|
||||
}
|
||||
|
||||
const padSize = encryptedData.length % 16
|
||||
if (padSize !== 0) {
|
||||
// data came from a codec that uses non-16-based padding.
|
||||
// it is safe to drop those padding bytes
|
||||
encryptedData = encryptedData.slice(0, -padSize)
|
||||
}
|
||||
|
||||
const ige = await createAesIgeForMessage(
|
||||
this._crypto,
|
||||
this._authKey!,
|
||||
|
|
|
@ -84,6 +84,9 @@ export interface PacketCodec {
|
|||
/** Reset codec state (for example, reset buffer) */
|
||||
reset(): void
|
||||
|
||||
/** Remove all listeners from a codec, used to safely dispose it */
|
||||
removeAllListeners(): void
|
||||
|
||||
/**
|
||||
* Emitted when a packet containing a
|
||||
* (Transport error)[https://core.telegram.org/mtproto/mtproto-transports#transport-errors] is encountered.
|
||||
|
|
|
@ -2,16 +2,17 @@ import { TransportFactory } from './abstract'
|
|||
|
||||
export * from './abstract'
|
||||
export * from './streamed'
|
||||
export * from './wrapped'
|
||||
export * from './tcp'
|
||||
export * from './tcp-intermediate'
|
||||
export * from './websocket'
|
||||
export * from './ws-obfuscated'
|
||||
export * from './intermediate'
|
||||
export * from './obfuscated'
|
||||
|
||||
/** Platform-defined default transport factory */
|
||||
export let defaultTransportFactory: TransportFactory
|
||||
if (typeof process !== 'undefined') {
|
||||
// we are in node, use tcp transport by default
|
||||
const { TcpIntermediateTransport } = require('./tcp-intermediate')
|
||||
const { TcpIntermediateTransport } = require('./tcp')
|
||||
defaultTransportFactory = () => new TcpIntermediateTransport()
|
||||
} else {
|
||||
// we are in browser (probably), use websocket
|
||||
|
@ -24,7 +25,7 @@ if (typeof process !== 'undefined') {
|
|||
)
|
||||
}
|
||||
} else {
|
||||
const { WebSocketObfuscatedTransport } = require('./ws-obfuscated')
|
||||
defaultTransportFactory = () => new WebSocketObfuscatedTransport()
|
||||
const { WebSocketIntermediateTransport } = require('./websocket')
|
||||
defaultTransportFactory = () => new WebSocketIntermediateTransport()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { PacketCodec, TransportError } from './abstract'
|
||||
import { StreamedCodec } from './streamed'
|
||||
import { TcpTransport } from './tcp'
|
||||
import { randomBytes } from '../../utils/buffer-utils'
|
||||
|
||||
const TAG = Buffer.from([0xee, 0xee, 0xee, 0xee])
|
||||
const PADDED_TAG = Buffer.from([0xdd, 0xdd, 0xdd, 0xdd])
|
||||
|
||||
/**
|
||||
* Intermediate packet codec.
|
||||
|
@ -30,13 +31,11 @@ export class IntermediatePacketCodec
|
|||
const payloadLength = this._stream.readUInt32LE(0)
|
||||
|
||||
if (payloadLength <= this._stream.length - 4) {
|
||||
const payload = this._stream.slice(4, payloadLength + 4)
|
||||
|
||||
if (payloadLength === 4) {
|
||||
const code = this._stream.readInt32LE(4) * -1
|
||||
|
||||
this.emit('error', new TransportError(code))
|
||||
} else {
|
||||
const payload = this._stream.slice(4, payloadLength + 4)
|
||||
this.emit('packet', payload)
|
||||
}
|
||||
|
||||
|
@ -48,6 +47,24 @@ export class IntermediatePacketCodec
|
|||
}
|
||||
}
|
||||
|
||||
export class TcpIntermediateTransport extends TcpTransport {
|
||||
_packetCodec = new IntermediatePacketCodec()
|
||||
/**
|
||||
* Padded intermediate packet codec.
|
||||
* See https://core.telegram.org/mtproto/mtproto-transports#padded-intermediate
|
||||
*/
|
||||
export class PaddedIntermediatePacketCodec extends IntermediatePacketCodec {
|
||||
tag(): Buffer {
|
||||
return PADDED_TAG
|
||||
}
|
||||
|
||||
encode(packet: Buffer): Buffer {
|
||||
// padding size, 0-15
|
||||
const padSize = Math.floor(Math.random() * 16)
|
||||
const padding = randomBytes(padSize)
|
||||
|
||||
const ret = Buffer.alloc(packet.length + 4 + padSize)
|
||||
ret.writeUInt32LE(packet.length + padSize)
|
||||
packet.copy(ret, 4)
|
||||
padding.copy(ret, 4 + ret.length)
|
||||
return ret
|
||||
}
|
||||
}
|
|
@ -1,12 +1,8 @@
|
|||
import { PacketCodec } from './abstract'
|
||||
import { ICryptoProvider, IEncryptionScheme } from '../../utils/crypto'
|
||||
import { EventEmitter } from 'events'
|
||||
import {
|
||||
buffersEqual,
|
||||
randomBytes,
|
||||
} from '../../utils/buffer-utils'
|
||||
import { WebSocketTransport } from './websocket'
|
||||
import { IntermediatePacketCodec } from './tcp-intermediate'
|
||||
import { buffersEqual, randomBytes } from '../../utils/buffer-utils'
|
||||
import { WrappedCodec } from './wrapped'
|
||||
|
||||
// initial payload can't start with these
|
||||
const BAD_HEADERS = [
|
||||
|
@ -17,21 +13,22 @@ const BAD_HEADERS = [
|
|||
Buffer.from('eeeeeeee', 'hex'),
|
||||
]
|
||||
|
||||
export class ObfuscatedPacketCodec extends EventEmitter implements PacketCodec {
|
||||
private _inner: PacketCodec
|
||||
private _crypto: ICryptoProvider
|
||||
interface MtProxyInfo {
|
||||
dcId: number
|
||||
secret: Buffer
|
||||
test: boolean
|
||||
media: boolean
|
||||
}
|
||||
|
||||
export class ObfuscatedPacketCodec extends WrappedCodec implements PacketCodec {
|
||||
private _encryptor?: IEncryptionScheme
|
||||
private _decryptor?: IEncryptionScheme
|
||||
|
||||
constructor(inner: PacketCodec) {
|
||||
super()
|
||||
this._inner = inner
|
||||
this._inner.on('error', (err) => this.emit('error', err))
|
||||
this._inner.on('packet', (buf) => this.emit('packet', buf))
|
||||
}
|
||||
private _proxy?: MtProxyInfo
|
||||
|
||||
setupCrypto(crypto: ICryptoProvider): void {
|
||||
this._crypto = crypto
|
||||
constructor(inner: PacketCodec, proxy?: MtProxyInfo) {
|
||||
super(inner)
|
||||
this._proxy = proxy
|
||||
}
|
||||
|
||||
async tag(): Promise<Buffer> {
|
||||
|
@ -54,16 +51,37 @@ export class ObfuscatedPacketCodec extends EventEmitter implements PacketCodec {
|
|||
}
|
||||
innerTag.copy(random, 56)
|
||||
|
||||
if (this._proxy) {
|
||||
let dcId = this._proxy.dcId
|
||||
if (this._proxy.test) dcId += 10000
|
||||
if (this._proxy.media) dcId = -dcId
|
||||
|
||||
random.writeInt16LE(dcId, 60)
|
||||
}
|
||||
|
||||
const randomRev = Buffer.from(random.slice(8, 56)).reverse()
|
||||
|
||||
const encryptKey = random.slice(8, 40)
|
||||
let encryptKey = random.slice(8, 40)
|
||||
const encryptIv = random.slice(40, 56)
|
||||
|
||||
const decryptKey = randomRev.slice(0, 32)
|
||||
let decryptKey = randomRev.slice(0, 32)
|
||||
const decryptIv = randomRev.slice(32, 48)
|
||||
|
||||
if (this._proxy) {
|
||||
encryptKey = await this._crypto.sha256(
|
||||
Buffer.concat([encryptKey, this._proxy.secret])
|
||||
)
|
||||
decryptKey = await this._crypto.sha256(
|
||||
Buffer.concat([decryptKey, this._proxy.secret])
|
||||
)
|
||||
}
|
||||
|
||||
this._encryptor = this._crypto.createAesCtr(encryptKey, encryptIv, true)
|
||||
this._decryptor = this._crypto.createAesCtr(decryptKey, decryptIv, false)
|
||||
this._decryptor = this._crypto.createAesCtr(
|
||||
decryptKey,
|
||||
decryptIv,
|
||||
false
|
||||
)
|
||||
|
||||
const encrypted = await this._encryptor.encrypt(random)
|
||||
encrypted.copy(random, 56, 56, 64)
|
||||
|
@ -87,7 +105,3 @@ export class ObfuscatedPacketCodec extends EventEmitter implements PacketCodec {
|
|||
delete this._decryptor
|
||||
}
|
||||
}
|
||||
|
||||
export class WebSocketObfuscatedTransport extends WebSocketTransport {
|
||||
_packetCodec = new ObfuscatedPacketCodec(new IntermediatePacketCodec())
|
||||
}
|
|
@ -3,6 +3,7 @@ import { tl } from '@mtcute/tl'
|
|||
import { Socket, connect } from 'net'
|
||||
import EventEmitter from 'events'
|
||||
import { ICryptoProvider } from '../../utils/crypto'
|
||||
import { IntermediatePacketCodec } from './intermediate'
|
||||
|
||||
const debug = require('debug')('mtcute:tcp')
|
||||
|
||||
|
@ -39,6 +40,7 @@ export abstract class TcpTransport
|
|||
throw new Error('Transport is not IDLE')
|
||||
|
||||
if (!this.packetCodecInitialized) {
|
||||
this._packetCodec.setupCrypto?.(this._crypto)
|
||||
this._packetCodec.on('error', (err) => this.emit('error', err))
|
||||
this._packetCodec.on('packet', (buf) => this.emit('message', buf))
|
||||
this.packetCodecInitialized = true
|
||||
|
@ -79,15 +81,20 @@ export abstract class TcpTransport
|
|||
debug('%s: connected', this._currentDc!.ipAddress)
|
||||
const initialMessage = await this._packetCodec.tag()
|
||||
|
||||
this._socket!.write(initialMessage, (err) => {
|
||||
if (err) {
|
||||
this.emit('error', err)
|
||||
this.close()
|
||||
} else {
|
||||
this._state = TransportState.Ready
|
||||
this.emit('ready')
|
||||
}
|
||||
})
|
||||
if (initialMessage.length) {
|
||||
this._socket!.write(initialMessage, (err) => {
|
||||
if (err) {
|
||||
this.emit('error', err)
|
||||
this.close()
|
||||
} else {
|
||||
this._state = TransportState.Ready
|
||||
this.emit('ready')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this._state = TransportState.Ready
|
||||
this.emit('ready')
|
||||
}
|
||||
}
|
||||
|
||||
async send(bytes: Buffer): Promise<void> {
|
||||
|
@ -101,3 +108,7 @@ export abstract class TcpTransport
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class TcpIntermediateTransport extends TcpTransport {
|
||||
_packetCodec = new IntermediatePacketCodec()
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import EventEmitter from 'events'
|
|||
import { typedArrayToBuffer } from '../../utils/buffer-utils'
|
||||
import { ICryptoProvider } from '../../utils/crypto'
|
||||
import type WebSocket from 'ws'
|
||||
import { IntermediatePacketCodec } from './intermediate'
|
||||
import { ObfuscatedPacketCodec } from './obfuscated'
|
||||
|
||||
const debug = require('debug')('mtcute:ws')
|
||||
|
||||
|
@ -41,7 +43,7 @@ export abstract class WebSocketTransport
|
|||
private _currentDc: tl.RawDcOption | null = null
|
||||
private _state: TransportState = TransportState.Idle
|
||||
private _socket: WebSocket | null = null
|
||||
private _crypto?: ICryptoProvider
|
||||
private _crypto: ICryptoProvider
|
||||
|
||||
abstract _packetCodec: PacketCodec
|
||||
packetCodecInitialized = false
|
||||
|
@ -91,7 +93,7 @@ export abstract class WebSocketTransport
|
|||
throw new Error('Transport is not IDLE')
|
||||
|
||||
if (!this.packetCodecInitialized) {
|
||||
if (this._crypto) this._packetCodec.setupCrypto?.(this._crypto)
|
||||
this._packetCodec.setupCrypto?.(this._crypto)
|
||||
this._packetCodec.on('error', (err) => this.emit('error', err))
|
||||
this._packetCodec.on('packet', (buf) => this.emit('message', buf))
|
||||
this.packetCodecInitialized = true
|
||||
|
@ -149,3 +151,7 @@ export abstract class WebSocketTransport
|
|||
this._socket!.send(framed)
|
||||
}
|
||||
}
|
||||
|
||||
export class WebSocketIntermediateTransport extends WebSocketTransport {
|
||||
_packetCodec = new ObfuscatedPacketCodec(new IntermediatePacketCodec())
|
||||
}
|
||||
|
|
27
packages/core/src/network/transports/wrapped.ts
Normal file
27
packages/core/src/network/transports/wrapped.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { PacketCodec } from './abstract'
|
||||
import { ICryptoProvider } from '../../utils/crypto'
|
||||
|
||||
export abstract class WrappedCodec extends EventEmitter {
|
||||
protected _crypto: ICryptoProvider
|
||||
protected _inner: PacketCodec
|
||||
|
||||
constructor (inner: PacketCodec) {
|
||||
super()
|
||||
this._inner = inner
|
||||
this._inner.on('error', (err) => this.emit('error', err))
|
||||
this._inner.on('packet', (buf) => this.emit('packet', buf))
|
||||
}
|
||||
|
||||
removeAllListeners(): this {
|
||||
super.removeAllListeners()
|
||||
this._inner.removeAllListeners()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setupCrypto(crypto: ICryptoProvider): void {
|
||||
this._crypto = crypto
|
||||
this._inner.setupCrypto?.(crypto)
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ export interface ICryptoProvider {
|
|||
iterations: number
|
||||
): MaybeAsync<Buffer>
|
||||
rsaEncrypt(data: Buffer, key: TlPublicKey): MaybeAsync<Buffer>
|
||||
hmacSha256(data: Buffer, key: Buffer): MaybeAsync<Buffer>
|
||||
|
||||
// in telegram, iv is always either used only once, or is the same for all calls for the key
|
||||
createAesCtr(key: Buffer, iv: Buffer, encrypt: boolean): IEncryptionScheme
|
||||
|
@ -79,6 +80,8 @@ export abstract class BaseCryptoProvider implements ICryptoProvider {
|
|||
|
||||
abstract sha256(data: Buffer): MaybeAsync<Buffer>
|
||||
|
||||
abstract hmacSha256(data: Buffer, key: Buffer): MaybeAsync<Buffer>
|
||||
|
||||
abstract createMd5(): IHashMethod
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,9 @@ export class ForgeCryptoProvider extends BaseCryptoProvider {
|
|||
}
|
||||
|
||||
createAesCtr(key: Buffer, iv: Buffer, encrypt: boolean): IEncryptionScheme {
|
||||
const cipher = forge.cipher[encrypt ? 'createCipher' : 'createDecipher']('AES-CTR', key.toString('binary'))
|
||||
const cipher = forge.cipher[
|
||||
encrypt ? 'createCipher' : 'createDecipher'
|
||||
]('AES-CTR', key.toString('binary'))
|
||||
cipher.start({ iv: iv.toString('binary') })
|
||||
|
||||
const update = (data: Buffer): Buffer => {
|
||||
|
@ -27,7 +29,7 @@ export class ForgeCryptoProvider extends BaseCryptoProvider {
|
|||
|
||||
return {
|
||||
encrypt: update,
|
||||
decrypt: update
|
||||
decrypt: update,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,10 +46,7 @@ export class ForgeCryptoProvider extends BaseCryptoProvider {
|
|||
return Buffer.from(cipher.output.data, 'binary')
|
||||
},
|
||||
decrypt(data: Buffer) {
|
||||
const cipher = forge.cipher.createDecipher(
|
||||
'AES-ECB',
|
||||
keyBuffer
|
||||
)
|
||||
const cipher = forge.cipher.createDecipher('AES-ECB', keyBuffer)
|
||||
cipher.start({})
|
||||
cipher.mode.pad = cipher.mode.unpad = false
|
||||
cipher.update(forge.util.createBuffer(data.toString('binary')))
|
||||
|
@ -100,4 +99,14 @@ export class ForgeCryptoProvider extends BaseCryptoProvider {
|
|||
digest: () => Buffer.from(hash.digest().data, 'binary'),
|
||||
}
|
||||
}
|
||||
|
||||
hmacSha256(data: Buffer, key: Buffer): MaybeAsync<Buffer> {
|
||||
const hmac = forge.hmac.create()
|
||||
hmac.start('sha256', key.toString('binary'))
|
||||
hmac.update(data.toString('binary'))
|
||||
return Buffer.from(
|
||||
hmac.digest().data,
|
||||
'binary'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ export class NodeCryptoProvider extends BaseCryptoProvider {
|
|||
}
|
||||
|
||||
createAesCtr(key: Buffer, iv: Buffer, encrypt: boolean): IEncryptionScheme {
|
||||
const cipher = nodeCrypto[encrypt ? 'createCipheriv' : 'createDecipheriv'](`aes-${key.length * 8}-ctr`, key, iv)
|
||||
const cipher = nodeCrypto[
|
||||
encrypt ? 'createCipheriv' : 'createDecipheriv'
|
||||
](`aes-${key.length * 8}-ctr`, key, iv)
|
||||
|
||||
const update = (data: Buffer) => cipher.update(data)
|
||||
|
||||
|
@ -30,7 +32,11 @@ export class NodeCryptoProvider extends BaseCryptoProvider {
|
|||
return Buffer.concat([cipher.update(data), cipher.final()])
|
||||
},
|
||||
decrypt(data: Buffer) {
|
||||
const cipher = nodeCrypto.createDecipheriv(methodName, key, null)
|
||||
const cipher = nodeCrypto.createDecipheriv(
|
||||
methodName,
|
||||
key,
|
||||
null
|
||||
)
|
||||
cipher.setAutoPadding(false)
|
||||
return Buffer.concat([cipher.update(data), cipher.final()])
|
||||
},
|
||||
|
@ -66,4 +72,8 @@ export class NodeCryptoProvider extends BaseCryptoProvider {
|
|||
createMd5(): IHashMethod {
|
||||
return nodeCrypto.createHash('md5')
|
||||
}
|
||||
|
||||
hmacSha256(data: Buffer, key: Buffer): MaybeAsync<Buffer> {
|
||||
return nodeCrypto.createHmac('sha256', key).update(data).digest()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,28 @@ export function testCryptoProvider(c: ICryptoProvider): void {
|
|||
)
|
||||
})
|
||||
|
||||
it('should calculate hmac-sha256', async () => {
|
||||
const key = Buffer.from('aaeeff', 'hex')
|
||||
|
||||
expect(
|
||||
(await c.hmacSha256(Buffer.from(''), key)).toString('hex')
|
||||
).to.eq(
|
||||
'642711307c9e4437df09d6ebaa6bdc1b3a810c7f15c50fd1d0f8d7d5490f44dd'
|
||||
)
|
||||
expect(
|
||||
(await c.hmacSha256(Buffer.from('hello'), key)).toString('hex')
|
||||
).to.eq(
|
||||
'39b00bab151f9868e6501655c580b5542954711181243474d46b894703b1c1c2'
|
||||
)
|
||||
expect(
|
||||
(await c.hmacSha256(Buffer.from('aebb1f', 'hex'), key)).toString(
|
||||
'hex'
|
||||
)
|
||||
).to.eq(
|
||||
'a3a7273871808711cab17aba14f58e96f63f3ccfc5097d206f0f00ead2c3dd35'
|
||||
)
|
||||
})
|
||||
|
||||
it('should derive pbkdf2 key', async () => {
|
||||
expect(
|
||||
(
|
||||
|
@ -47,35 +69,68 @@ export function testCryptoProvider(c: ICryptoProvider): void {
|
|||
|
||||
it('should encrypt and decrypt aes-ctr', async () => {
|
||||
let aes = c.createAesCtr(
|
||||
Buffer.from('d450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b', 'hex'),
|
||||
Buffer.from(
|
||||
'd450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b',
|
||||
'hex'
|
||||
),
|
||||
Buffer.from('0182de2bd789c295c3c6c875c5e9e190', 'hex'),
|
||||
true
|
||||
)
|
||||
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('a5fea1')
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('ab51ca')
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('365e5c')
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('4b94a9')
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('776387')
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq('c940be')
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
|
||||
'a5fea1'
|
||||
)
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
|
||||
'ab51ca'
|
||||
)
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
|
||||
'365e5c'
|
||||
)
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
|
||||
'4b94a9'
|
||||
)
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
|
||||
'776387'
|
||||
)
|
||||
expect((await aes.encrypt(Buffer.from([1, 2, 3]))).toString('hex')).eq(
|
||||
'c940be'
|
||||
)
|
||||
|
||||
aes = c.createAesCtr(
|
||||
Buffer.from('d450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b', 'hex'),
|
||||
Buffer.from(
|
||||
'd450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b',
|
||||
'hex'
|
||||
),
|
||||
Buffer.from('0182de2bd789c295c3c6c875c5e9e190', 'hex'),
|
||||
false
|
||||
)
|
||||
|
||||
expect((await aes.decrypt(Buffer.from('a5fea1', 'hex'))).toString('hex')).eq('010203')
|
||||
expect((await aes.decrypt(Buffer.from('ab51ca', 'hex'))).toString('hex')).eq('010203')
|
||||
expect((await aes.decrypt(Buffer.from('365e5c', 'hex'))).toString('hex')).eq('010203')
|
||||
expect((await aes.decrypt(Buffer.from('4b94a9', 'hex'))).toString('hex')).eq('010203')
|
||||
expect((await aes.decrypt(Buffer.from('776387', 'hex'))).toString('hex')).eq('010203')
|
||||
expect((await aes.decrypt(Buffer.from('c940be', 'hex'))).toString('hex')).eq('010203')
|
||||
expect(
|
||||
(await aes.decrypt(Buffer.from('a5fea1', 'hex'))).toString('hex')
|
||||
).eq('010203')
|
||||
expect(
|
||||
(await aes.decrypt(Buffer.from('ab51ca', 'hex'))).toString('hex')
|
||||
).eq('010203')
|
||||
expect(
|
||||
(await aes.decrypt(Buffer.from('365e5c', 'hex'))).toString('hex')
|
||||
).eq('010203')
|
||||
expect(
|
||||
(await aes.decrypt(Buffer.from('4b94a9', 'hex'))).toString('hex')
|
||||
).eq('010203')
|
||||
expect(
|
||||
(await aes.decrypt(Buffer.from('776387', 'hex'))).toString('hex')
|
||||
).eq('010203')
|
||||
expect(
|
||||
(await aes.decrypt(Buffer.from('c940be', 'hex'))).toString('hex')
|
||||
).eq('010203')
|
||||
})
|
||||
|
||||
it('should encrypt and decrypt aes-ige', async () => {
|
||||
const aes = c.createAesIge(
|
||||
Buffer.from('5468697320697320616E20696D706C655468697320697320616E20696D706C65', 'hex'),
|
||||
Buffer.from(
|
||||
'5468697320697320616E20696D706C655468697320697320616E20696D706C65',
|
||||
'hex'
|
||||
),
|
||||
Buffer.from(
|
||||
'6D656E746174696F6E206F6620494745206D6F646520666F72204F70656E5353',
|
||||
'hex'
|
||||
|
|
351
packages/mtproxy/fake-tls.ts
Normal file
351
packages/mtproxy/fake-tls.ts
Normal file
|
@ -0,0 +1,351 @@
|
|||
import {
|
||||
bigIntToBuffer,
|
||||
bufferToBigInt,
|
||||
ICryptoProvider,
|
||||
PacketCodec,
|
||||
WrappedCodec,
|
||||
randomBytes
|
||||
} from '@mtcute/core'
|
||||
import bigInt, { BigInteger } from 'big-integer'
|
||||
|
||||
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 = bigInt(
|
||||
'7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed',
|
||||
16
|
||||
)
|
||||
// 2^255 - 19
|
||||
const QUAD_RES_MOD = bigInt('7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed', 16)
|
||||
// (mod - 1) / 2 = 2^254 - 10
|
||||
const QUAD_RES_POW = bigInt('3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6', 16)
|
||||
|
||||
function _getY2(x: BigInteger, mod: BigInteger): BigInteger {
|
||||
// returns y = x^3 + x^2 * 486662 + x
|
||||
let y = x
|
||||
y = y.add(486662).mod(mod)
|
||||
y = y.multiply(x).mod(mod)
|
||||
y = y.plus(1).mod(mod)
|
||||
y = y.multiply(x).mod(mod)
|
||||
|
||||
return y
|
||||
}
|
||||
|
||||
function _getDoubleX(x: BigInteger, mod: BigInteger): BigInteger {
|
||||
// returns x_2 = (x^2 - 1)^2/(4*y^2)
|
||||
let denominator = _getY2(x, mod)
|
||||
denominator = denominator.multiply(4).mod(mod)
|
||||
|
||||
let numerator = x.multiply(x).mod(mod)
|
||||
numerator = numerator.minus(1).mod(mod)
|
||||
numerator = numerator.multiply(numerator).mod(mod)
|
||||
|
||||
denominator = denominator.modInv(mod)
|
||||
numerator = numerator.multiply(denominator).mod(mod)
|
||||
|
||||
return numerator
|
||||
}
|
||||
|
||||
function _isQuadraticResidue(a: BigInteger): boolean {
|
||||
const r = a.modPow(QUAD_RES_POW, QUAD_RES_MOD)
|
||||
|
||||
return r.eq(1)
|
||||
}
|
||||
|
||||
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(size: number): Buffer {
|
||||
const buf = 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 implements TlsOperationHandler {
|
||||
buf: Buffer
|
||||
pos = 0
|
||||
|
||||
private _domain: Buffer
|
||||
private _grease = initGrease(7)
|
||||
private _scopes: number[] = []
|
||||
|
||||
constructor(size: number, domain: Buffer) {
|
||||
this._domain = domain
|
||||
this.buf = Buffer.allocUnsafe(size)
|
||||
}
|
||||
|
||||
string(buf: Buffer) {
|
||||
buf.copy(this.buf, this.pos)
|
||||
this.pos += buf.length
|
||||
}
|
||||
|
||||
random(size: number) {
|
||||
this.string(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 = 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()!
|
||||
const end = this.pos
|
||||
const size = end - begin - 2
|
||||
|
||||
this.buf.writeUInt16BE(size, begin)
|
||||
}
|
||||
|
||||
async finish(secret: Buffer, crypto: ICryptoProvider): Promise<Buffer> {
|
||||
const padSize = 515 - this.pos
|
||||
const unixTime = ~~(Date.now() / 1000)
|
||||
|
||||
this.beginScope()
|
||||
this.zero(padSize)
|
||||
this.endScope()
|
||||
|
||||
const hash = await 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(517, domainBuf)
|
||||
executeTlsOperations(writer)
|
||||
return writer.finish(secret, crypto)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake TLS packet codec, used for some MTProxies.
|
||||
*
|
||||
* Must only be used inside {@link MtProxyTcpTransport}
|
||||
* @internal
|
||||
*/
|
||||
export class FakeTlsPacketCodec extends WrappedCodec implements PacketCodec {
|
||||
protected _stream = Buffer.alloc(0)
|
||||
|
||||
private _header: Buffer
|
||||
private _isFirstTls = true
|
||||
|
||||
async tag(): Promise<Buffer> {
|
||||
this._header = 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])
|
||||
} else {
|
||||
return Buffer.concat([header, packet])
|
||||
}
|
||||
}
|
||||
|
||||
async encode(packet: Buffer): Promise<Buffer> {
|
||||
packet = 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
|
||||
}
|
||||
}
|
213
packages/mtproxy/index.ts
Normal file
213
packages/mtproxy/index.ts
Normal file
|
@ -0,0 +1,213 @@
|
|||
import {
|
||||
IntermediatePacketCodec,
|
||||
ObfuscatedPacketCodec,
|
||||
PacketCodec,
|
||||
PaddedIntermediatePacketCodec,
|
||||
parseUrlSafeBase64,
|
||||
TcpTransport,
|
||||
TransportState,
|
||||
} from '@mtcute/core'
|
||||
import { tl } from '@mtcute/tl'
|
||||
import { connect } from 'net'
|
||||
import { FakeTlsPacketCodec, generateFakeTlsHeader } from './fake-tls'
|
||||
|
||||
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 a SOCKS4/5 proxy.
|
||||
*/
|
||||
export class MtProxyTcpTransport extends TcpTransport {
|
||||
readonly _proxy: MtProxySettings
|
||||
|
||||
private _rawSecret: Buffer
|
||||
private _randomPadding = false
|
||||
private _fakeTlsDomain: string | null = null
|
||||
private _test: boolean
|
||||
|
||||
/**
|
||||
* @param proxy Information about the proxy
|
||||
* @param test Whether test servers should be used
|
||||
*/
|
||||
constructor(proxy: MtProxySettings, test = false) {
|
||||
super()
|
||||
|
||||
this._test = test
|
||||
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 = parseUrlSafeBase64(proxy.secret)
|
||||
}
|
||||
|
||||
if (secret.length > 17 + MAX_DOMAIN_LENGTH) {
|
||||
throw new Error('Invalid secret: too long')
|
||||
}
|
||||
|
||||
if (secret.length < 16) {
|
||||
throw new Error('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 Error('Unsupported secret')
|
||||
}
|
||||
}
|
||||
|
||||
_packetCodec!: PacketCodec
|
||||
|
||||
connect(dc: tl.RawDcOption): void {
|
||||
if (this._state !== TransportState.Idle)
|
||||
throw new Error('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 any)._packetCodec
|
||||
}
|
||||
|
||||
if (!this._packetCodec) {
|
||||
const proxy = {
|
||||
dcId: dc.id,
|
||||
media: dc.mediaOnly!,
|
||||
test: this._test,
|
||||
secret: this._rawSecret,
|
||||
}
|
||||
|
||||
if (!this._fakeTlsDomain) {
|
||||
let inner: PacketCodec
|
||||
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.setupCrypto?.(this._crypto)
|
||||
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,
|
||||
this._handleConnectFakeTls.bind(this)
|
||||
)
|
||||
} else {
|
||||
this._socket = connect(
|
||||
this._proxy.port,
|
||||
this._proxy.host,
|
||||
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)
|
||||
|
||||
const checkHelloResponse = async (buf: Buffer): Promise<void> => {
|
||||
const resp = buf
|
||||
for (const first of TLS_START) {
|
||||
if (buf.length < first.length + 2) {
|
||||
return
|
||||
}
|
||||
|
||||
if (first.compare(first, 0, first.length) !== 0) {
|
||||
throw new Error('First part of hello response is invalid')
|
||||
}
|
||||
buf = buf.slice(first.length)
|
||||
|
||||
const skipSize = buf.readUInt16BE()
|
||||
buf = buf.slice(2)
|
||||
if (buf.length < skipSize) {
|
||||
return
|
||||
}
|
||||
|
||||
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 (hash.compare(respRand) !== 0) {
|
||||
throw new Error('Response hash is invalid')
|
||||
}
|
||||
}
|
||||
|
||||
const packetHandler = async (buf: Buffer): Promise<void> => {
|
||||
try {
|
||||
await checkHelloResponse(buf)
|
||||
|
||||
this._socket!.on('data', (data) => this._packetCodec.feed(data))
|
||||
this._socket!.off('data', packetHandler)
|
||||
this.handleConnect()
|
||||
} catch (e) {
|
||||
this._socket!.emit('error', e)
|
||||
}
|
||||
}
|
||||
|
||||
this._socket!.write(hello)
|
||||
this._socket!.on('data', packetHandler)
|
||||
} catch (e) {
|
||||
this._socket!.emit('error', e)
|
||||
}
|
||||
}
|
||||
}
|
17
packages/mtproxy/package.json
Normal file
17
packages/mtproxy/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@mtcute/mtproxy",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"description": "MTProto proxy (MTProxy) 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/mtproxy/tsconfig.json
Normal file
19
packages/mtproxy/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"./index.ts",
|
||||
],
|
||||
"typedocOptions": {
|
||||
"name": "@mtcute/mtproxy",
|
||||
"includeVersion": true,
|
||||
"out": "../../docs/packages/mtproxy",
|
||||
"listInvalidSymbolLinks": true,
|
||||
"excludePrivate": true,
|
||||
"entryPoints": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue