Browse Source

Detect available transport

master
morethanwords 3 years ago
parent
commit
5451eefe80
  1. 2
      src/config/modes.ts
  2. 41
      src/lib/mtproto/apiManager.ts
  3. 31
      src/lib/mtproto/authorizer.ts
  4. 145
      src/lib/mtproto/networker.ts
  5. 11
      src/lib/mtproto/networkerFactory.ts
  6. 98
      src/lib/mtproto/transports/controller.ts
  7. 53
      src/lib/mtproto/transports/http.ts
  8. 23
      src/lib/mtproto/transports/tcpObfuscated.ts
  9. 4
      src/lib/mtproto/transports/transport.ts
  10. 8
      src/lib/mtproto/transports/websocket.ts
  11. 8
      webpack.common.js

2
src/config/modes.ts

@ -18,7 +18,7 @@ const Modes = {
ssl: true, // location.search.indexOf('ssl=1') > 0 || location.protocol === 'https:' && location.search.indexOf('ssl=0') === -1, ssl: true, // location.search.indexOf('ssl=1') > 0 || location.protocol === 'https:' && location.search.indexOf('ssl=0') === -1,
multipleConnections: true, multipleConnections: true,
asServiceWorker: false, asServiceWorker: false,
transport: 'https' as TransportType transport: 'websocket' as TransportType
}; };
/// #if MTPROTO_HAS_HTTP /// #if MTPROTO_HAS_HTTP

41
src/lib/mtproto/apiManager.ts

@ -36,6 +36,10 @@ import Modes from '../../config/modes';
import rootScope from '../rootScope'; import rootScope from '../rootScope';
/// #endif /// #endif
/// #if MTPROTO_AUTO
import transportController from './transports/controller';
/// #endif
/* var networker = apiManager.cachedNetworkers.websocket.upload[2]; /* var networker = apiManager.cachedNetworkers.websocket.upload[2];
networker.wrapMtpMessage({ networker.wrapMtpMessage({
_: 'msgs_state_req', _: 'msgs_state_req',
@ -106,6 +110,12 @@ export class ApiManager {
this.afterMessageTempIds = {}; this.afterMessageTempIds = {};
this.transportType = Modes.transport; this.transportType = Modes.transport;
/// #if MTPROTO_AUTO
transportController.addEventListener('transport', (transportType) => {
this.changeTransportType(transportType);
});
/// #endif
} }
//private lol = false; //private lol = false;
@ -168,6 +178,12 @@ export class ApiManager {
public changeTransportType(transportType: TransportType) { public changeTransportType(transportType: TransportType) {
const oldTransportType = this.transportType; const oldTransportType = this.transportType;
if(oldTransportType === transportType) {
return;
}
this.log('changing transport from', oldTransportType, 'to', transportType);
const oldObject = this.cachedNetworkers[oldTransportType]; const oldObject = this.cachedNetworkers[oldTransportType];
const newObject = this.cachedNetworkers[transportType]; const newObject = this.cachedNetworkers[transportType];
this.cachedNetworkers[transportType] = oldObject; this.cachedNetworkers[transportType] = oldObject;
@ -274,7 +290,7 @@ export class ApiManager {
//const connectionType: ConnectionType = 'client'; //const connectionType: ConnectionType = 'client';
const transportType = this.getTransportType(connectionType); const transportType = this.getTransportType(connectionType);
if(!this.cachedNetworkers.hasOwnProperty(transportType)) { if(!this.cachedNetworkers[transportType]) {
this.cachedNetworkers[transportType] = { this.cachedNetworkers[transportType] = {
client: {}, client: {},
download: {}, download: {},
@ -312,7 +328,7 @@ export class ApiManager {
const ak: DcAuthKey = `dc${dcId}_auth_key` as any; const ak: DcAuthKey = `dc${dcId}_auth_key` as any;
const ss: DcServerSalt = `dc${dcId}_server_salt` as any; const ss: DcServerSalt = `dc${dcId}_server_salt` as any;
const transport = this.chooseServer(dcId, connectionType, transportType); let transport = this.chooseServer(dcId, connectionType, transportType);
return this.gettingNetworkers[getKey] = Promise.all([ak, ss].map(key => sessionStorage.get(key))) return this.gettingNetworkers[getKey] = Promise.all([ak, ss].map(key => sessionStorage.get(key)))
.then(async([authKeyHex, serverSaltHex]) => { .then(async([authKeyHex, serverSaltHex]) => {
let networker: MTPNetworker; let networker: MTPNetworker;
@ -325,19 +341,17 @@ export class ApiManager {
const authKeyId = (await CryptoWorker.invokeCrypto('sha1-hash', authKey)).slice(-8); const authKeyId = (await CryptoWorker.invokeCrypto('sha1-hash', authKey)).slice(-8);
const serverSalt = bytesFromHex(serverSaltHex); const serverSalt = bytesFromHex(serverSaltHex);
networker = networkerFactory.getNetworker(dcId, authKey, authKeyId, serverSalt, transport, options); networker = networkerFactory.getNetworker(dcId, authKey, authKeyId, serverSalt, options);
} else { } else {
try { // if no saved state try { // if no saved state
const auth = await authorizer.auth(dcId); const auth = await authorizer.auth(dcId);
const storeObj = { sessionStorage.set({
[ak]: bytesToHex(auth.authKey), [ak]: bytesToHex(auth.authKey),
[ss]: bytesToHex(auth.serverSalt) [ss]: bytesToHex(auth.serverSalt)
}; });
sessionStorage.set(storeObj);
networker = networkerFactory.getNetworker(dcId, auth.authKey, auth.authKeyId, auth.serverSalt, transport, options); networker = networkerFactory.getNetworker(dcId, auth.authKey, auth.authKeyId, auth.serverSalt, options);
} catch(error) { } catch(error) {
this.log('Get networker error', error, (error as Error).stack); this.log('Get networker error', error, (error as Error).stack);
delete this.gettingNetworkers[getKey]; delete this.gettingNetworkers[getKey];
@ -345,6 +359,16 @@ export class ApiManager {
} }
} }
// ! cannot get it before this promise because simultaneous changeTransport will change nothing
const newTransportType = this.getTransportType(connectionType);
if(newTransportType !== transportType) {
transport.destroy();
DcConfigurator.removeTransport(dcConfigurator.chosenServers, transport);
transport = this.chooseServer(dcId, connectionType, newTransportType);
}
networker.changeTransport(transport);
/* networker.onConnectionStatusChange = (online) => { /* networker.onConnectionStatusChange = (online) => {
console.log('status:', online); console.log('status:', online);
}; */ }; */
@ -377,7 +401,6 @@ export class ApiManager {
networker.destroy(); networker.destroy();
networkerFactory.removeNetworker(networker); networkerFactory.removeNetworker(networker);
DcConfigurator.removeTransport(this.cachedNetworkers, networker); DcConfigurator.removeTransport(this.cachedNetworkers, networker);
DcConfigurator.removeTransport(dcConfigurator.chosenServers, networker.transport);
}; };
networker.setDrainTimeout(); networker.setDrainTimeout();

31
src/lib/mtproto/authorizer.ts

@ -10,7 +10,7 @@
*/ */
import { TLSerialization, TLDeserialization } from "./tl_utils"; import { TLSerialization, TLDeserialization } from "./tl_utils";
import dcConfigurator from "./dcConfigurator"; import dcConfigurator, { TransportType } from "./dcConfigurator";
import rsaKeysManager from "./rsaKeysManager"; import rsaKeysManager from "./rsaKeysManager";
import timeManager from "./timeManager"; import timeManager from "./timeManager";
@ -24,6 +24,10 @@ import { addPadding } from "./bin_utils";
import { Awaited, DcId } from "../../types"; import { Awaited, DcId } from "../../types";
import { ApiError } from "./apiManager"; import { ApiError } from "./apiManager";
/// #if MTPROTO_AUTO
import transportController from "./transports/controller";
/// #endif
/* let fNewNonce: any = bytesFromHex('8761970c24cb2329b5b2459752c502f3057cb7e8dbab200e526e8767fdc73b3c').reverse(); /* let fNewNonce: any = bytesFromHex('8761970c24cb2329b5b2459752c502f3057cb7e8dbab200e526e8767fdc73b3c').reverse();
let fNonce: any = bytesFromHex('b597720d11faa5914ef485c529cde414').reverse(); let fNonce: any = bytesFromHex('b597720d11faa5914ef485c529cde414').reverse();
let fResult: any = new Uint8Array(bytesFromHex('000000000000000001b473a0661b285e480000006324160514e4cd29c585f44e91a5fa110d7297b5c0c4134c84893db5715ecd56af5ed618082182053cc5de91cd00000015c4b51c02000000a5b7f709355fc30b216be86c022bb4c3')); let fResult: any = new Uint8Array(bytesFromHex('000000000000000001b473a0661b285e480000006324160514e4cd29c585f44e91a5fa110d7297b5c0c4134c84893db5715ecd56af5ed618082182053cc5de91cd00000015c4b51c02000000a5b7f709355fc30b216be86c022bb4c3'));
@ -104,6 +108,12 @@ export class Authorizer {
private log: ReturnType<typeof logger>; private log: ReturnType<typeof logger>;
private transportType: TransportType;
/// #if MTPROTO_AUTO
private getTransportTypePromise: Promise<void>;
/// #endif
constructor() { constructor() {
this.cached = {}; this.cached = {};
this.log = logger(`AUTHORIZER`, LogTypes.Error | LogTypes.Log); this.log = logger(`AUTHORIZER`, LogTypes.Error | LogTypes.Log);
@ -122,7 +132,7 @@ export class Authorizer {
resultArray.set(headerArray); resultArray.set(headerArray);
resultArray.set(requestArray, headerArray.length); resultArray.set(requestArray, headerArray.length);
const transport = dcConfigurator.chooseServer(dcId); const transport = dcConfigurator.chooseServer(dcId, 'client', this.transportType);
const baseError = { const baseError = {
code: 406, code: 406,
type: 'NETWORK_BAD_RESPONSE' type: 'NETWORK_BAD_RESPONSE'
@ -571,17 +581,26 @@ export class Authorizer {
} }
} }
/// #if MTPROTO_AUTO
private getTransportType() {
if(this.getTransportTypePromise) return this.getTransportTypePromise;
return this.getTransportTypePromise = transportController.pingTransports().then(({websocket}) => {
this.transportType = websocket ? 'websocket' : 'https';
});
}
/// #endif
public auth(dcId: DcId) { public auth(dcId: DcId) {
let promise = this.cached[dcId]; let promise = this.cached[dcId];
if(promise) { if(promise) {
return promise; return promise;
} }
if(!dcConfigurator.chooseServer(dcId)) {
throw new Error('[MT] No server found for dc ' + dcId);
}
promise = new Promise(async(resolve, reject) => { promise = new Promise(async(resolve, reject) => {
/// #if MTPROTO_AUTO
await this.getTransportType();
/// #endif
let error: ApiError; let error: ApiError;
let _try = 1; let _try = 1;
while(_try++ <= 3) { while(_try++ <= 3) {

145
src/lib/mtproto/networker.ts

@ -36,6 +36,7 @@ import { bigInt2str, rightShift_, str2bigInt } from '../../vendor/leemon';
import { forEachReverse } from '../../helpers/array'; import { forEachReverse } from '../../helpers/array';
import { ConnectionStatus } from './connectionStatus'; import { ConnectionStatus } from './connectionStatus';
import ctx from '../../environment/ctx'; import ctx from '../../environment/ctx';
import dcConfigurator, { DcConfigurator } from './dcConfigurator';
//console.error('networker included!', new Error().stack); //console.error('networker included!', new Error().stack);
@ -104,6 +105,7 @@ export default class MTPNetworker {
/// #if MTPROTO_HAS_HTTP /// #if MTPROTO_HAS_HTTP
private longPollInterval: number; private longPollInterval: number;
private longPollPending: number; private longPollPending: number;
private checkConnectionRetryAt: number;
private checkConnectionTimeout: number; private checkConnectionTimeout: number;
private checkConnectionPeriod = 0; private checkConnectionPeriod = 0;
private sleepAfter: number; private sleepAfter: number;
@ -148,7 +150,6 @@ export default class MTPNetworker {
private authKey: Uint8Array, private authKey: Uint8Array,
private authKeyId: Uint8Array, private authKeyId: Uint8Array,
serverSalt: Uint8Array, serverSalt: Uint8Array,
transport: MTTransport,
options: InvokeApiOptions = {} options: InvokeApiOptions = {}
) { ) {
this.authKeyUint8 = convertToUint8Array(this.authKey); this.authKeyUint8 = convertToUint8Array(this.authKey);
@ -178,8 +179,6 @@ export default class MTPNetworker {
// rootScope.offlineConnecting = true */ // rootScope.offlineConnecting = true */
// } // }
this.changeTransport(transport);
// * handle outcoming dead socket, server will close the connection // * handle outcoming dead socket, server will close the connection
// if((this.transport as TcpObfuscated).networker) { // if((this.transport as TcpObfuscated).networker) {
// this.disconnectDelay = /* (this.transport as TcpObfuscated).retryTimeout */75; // this.disconnectDelay = /* (this.transport as TcpObfuscated).retryTimeout */75;
@ -366,9 +365,9 @@ export default class MTPNetworker {
public changeTransport(transport?: MTTransport) { public changeTransport(transport?: MTTransport) {
const oldTransport = this.transport; const oldTransport = this.transport;
if(oldTransport) { if(oldTransport) {
if((oldTransport as TcpObfuscated).destroy) { oldTransport.destroy();
(oldTransport as TcpObfuscated).destroy();
} DcConfigurator.removeTransport(dcConfigurator.chosenServers, this.transport);
if(this.nextReqTimeout) { if(this.nextReqTimeout) {
clearTimeout(this.nextReqTimeout); clearTimeout(this.nextReqTimeout);
@ -394,22 +393,25 @@ export default class MTPNetworker {
return; return;
} }
transport.networker = this;
/// #if MTPROTO_HAS_HTTP /// #if MTPROTO_HAS_HTTP
/// #if MTPROTO_HAS_WS /// #if MTPROTO_HAS_WS
if(transport instanceof HTTP) { if(transport instanceof HTTP) {
/// #endif /// #endif
this.longPollInterval = ctx.setInterval(this.checkLongPoll, 10000); this.longPollInterval = ctx.setInterval(this.checkLongPoll, 10000);
this.checkLongPoll(); this.checkLongPoll();
this.checkConnection('changed transport');
/// #if MTPROTO_HAS_WS /// #if MTPROTO_HAS_WS
} }
/// #endif /// #endif
/// #endif /// #endif
transport.networker = this; if(transport.connected && (transport as TcpObfuscated).connection) {
if((transport as TcpObfuscated).connected) {
this.setConnectionStatus(ConnectionStatus.Connected); this.setConnectionStatus(ConnectionStatus.Connected);
} }
this.resend();
} }
public destroy() { public destroy() {
@ -419,12 +421,16 @@ export default class MTPNetworker {
public forceReconnectTimeout() { public forceReconnectTimeout() {
if((this.transport as TcpObfuscated).reconnect) { if((this.transport as TcpObfuscated).reconnect) {
(this.transport as TcpObfuscated).reconnect(); (this.transport as TcpObfuscated).reconnect();
} else {
this.resend();
} }
} }
public forceReconnect() { public forceReconnect() {
if((this.transport as TcpObfuscated).forceReconnect) { if((this.transport as TcpObfuscated).forceReconnect) {
(this.transport as TcpObfuscated).forceReconnect(); (this.transport as TcpObfuscated).forceReconnect();
} else {
this.checkConnection('force reconnect');
} }
} }
@ -603,6 +609,10 @@ export default class MTPNetworker {
body: serializer.getBytes(true) body: serializer.getBytes(true)
}; };
if(this.offline) {
this.setConnectionStatus(ConnectionStatus.Connecting);
}
this.sendEncryptedRequest(pingMessage).then(() => { this.sendEncryptedRequest(pingMessage).then(() => {
this.toggleOffline(false); this.toggleOffline(false);
}, () => { }, () => {
@ -612,91 +622,83 @@ export default class MTPNetworker {
}); });
}; };
private toggleOffline(enabled: boolean) { private toggleOffline(offline: boolean) {
// this.log('toggle ', enabled, this.dcId, this.iii) if(this.offline !== offline) {
if(this.offline !== undefined && this.offline === enabled) { this.offline = offline;
return false;
}
this.offline = enabled; if(offline) {
clearTimeout(this.nextReqTimeout);
if(this.offline) { this.nextReqTimeout = 0;
clearTimeout(this.nextReqTimeout); this.nextReq = 0;
this.nextReqTimeout = 0;
this.nextReq = 0;
if(this.checkConnectionPeriod < 1.5) { if(this.checkConnectionPeriod < 1.5) {
this.checkConnectionPeriod = 0; this.checkConnectionPeriod = 0;
} }
this.checkConnectionTimeout = ctx.setTimeout(this.checkConnection, this.checkConnectionPeriod * 1000 | 0); const delay = this.checkConnectionPeriod * 1000 | 0;
this.checkConnectionPeriod = Math.min(30, (1 + this.checkConnectionPeriod) * 1.5); this.checkConnectionRetryAt = Date.now() + delay;
this.setConnectionStatus(ConnectionStatus.Closed, this.checkConnectionRetryAt);
this.checkConnectionTimeout = ctx.setTimeout(this.checkConnection, delay);
this.checkConnectionPeriod = Math.min(30, (1 + this.checkConnectionPeriod) * 1.5);
/// #if !MTPROTO_WORKER /// #if !MTPROTO_WORKER
document.body.addEventListener('online', this.checkConnection, false); document.body.addEventListener('online', this.checkConnection, false);
document.body.addEventListener('focus', this.checkConnection, false); document.body.addEventListener('focus', this.checkConnection, false);
/// #endif /// #endif
} else { } else {
this.checkLongPoll(); this.setConnectionStatus(ConnectionStatus.Connected);
this.checkLongPoll();
this.scheduleRequest(); this.scheduleRequest();
/// #if !MTPROTO_WORKER /// #if !MTPROTO_WORKER
document.body.removeEventListener('online', this.checkConnection); document.body.removeEventListener('online', this.checkConnection);
document.body.removeEventListener('focus', this.checkConnection); document.body.removeEventListener('focus', this.checkConnection);
/// #endif /// #endif
clearTimeout(this.checkConnectionTimeout); clearTimeout(this.checkConnectionTimeout);
this.checkConnectionTimeout = undefined; this.checkConnectionTimeout = undefined;
}
} }
this.setConnectionStatus(offline ? ConnectionStatus.Closed : ConnectionStatus.Connected, offline ? this.checkConnectionRetryAt : undefined);
} }
private handleSentEncryptedRequestHTTP(promise: ReturnType<MTPNetworker['sendEncryptedRequest']>, message: MTMessage, noResponseMsgs: string[]) { private handleSentEncryptedRequestHTTP(promise: ReturnType<MTPNetworker['sendEncryptedRequest']>, message: MTMessage, noResponseMsgs: string[]) {
// let timeout = setTimeout(() => {
// this.log.error('handleSentEncryptedRequestHTTP timeout', promise, message, noResponseMsgs);
// }, 5e3);
promise.then((result) => { promise.then((result) => {
this.toggleOffline(false); this.toggleOffline(false);
// this.log('parse for', message); // this.log('parse for', message);
this.parseResponse(result).then((response) => { return this.parseResponse(result).then((response) => {
if(Modes.debug) { this.debug && this.log.debug('Server response', response);
this.log.debug('Server response', response);
}
this.processMessage(response.response, response.messageId, response.sessionId); this.processMessage(response.response, response.messageId, response.sessionId);
noResponseMsgs.forEach((msgId) => {
if(this.sentMessages[msgId]) {
const deferred = this.sentMessages[msgId].deferred;
delete this.sentMessages[msgId];
deferred.resolve();
}
});
this.checkLongPoll(); this.checkLongPoll();
this.checkConnectionPeriod = Math.max(1.1, Math.sqrt(this.checkConnectionPeriod)); this.checkConnectionPeriod = Math.max(1.1, Math.sqrt(this.checkConnectionPeriod));
return true;
}); });
}, (error) => { }, (error) => {
this.log.error('Encrypted request failed', error, message); this.log.error('Encrypted request failed', error, message);
if(message.container) { this.pushResend(message.msg_id);
message.inner.forEach((msgId) => { this.toggleOffline(true);
this.pendingMessages[msgId] = 0;
});
delete this.sentMessages[message.msg_id];
} else {
this.pendingMessages[message.msg_id] = 0;
}
return false;
}).then((shouldResolve) => {
// clearTimeout(timeout);
noResponseMsgs.forEach((msgId) => { noResponseMsgs.forEach((msgId) => {
if(this.sentMessages[msgId]) { if(this.sentMessages[msgId]) {
const deferred = this.sentMessages[msgId].deferred; const deferred = this.sentMessages[msgId].deferred;
delete this.sentMessages[msgId]; delete this.sentMessages[msgId];
delete this.pendingMessages[msgId]; delete this.pendingMessages[msgId];
deferred.reject(); shouldResolve ? deferred.resolve() : deferred.reject();
} }
}); });
this.toggleOffline(true);
}); });
} }
/// #endif /// #endif
@ -1187,9 +1189,9 @@ export default class MTPNetworker {
private sendEncryptedRequest(message: MTMessage) { private sendEncryptedRequest(message: MTMessage) {
return this.getEncryptedOutput(message).then(requestData => { return this.getEncryptedOutput(message).then(requestData => {
this.debug && this.log.debug('sendEncryptedRequest: launching message into space:', message, [message.msg_id].concat(message.inner || [])); this.debug && this.log.debug('sendEncryptedRequest: launching message into space:', message, [message.msg_id].concat(message.inner || []), requestData.length);
const promise: Promise<Uint8Array> = this.transport.send(requestData) as any; const promise: Promise<Uint8Array> = this.transport.send(requestData) as any;
// this.debug && this.log.debug('sendEncryptedRequest: launched message into space:', message, promise);
/// #if !MTPROTO_HAS_HTTP /// #if !MTPROTO_HAS_HTTP
return promise; return promise;
@ -1204,20 +1206,23 @@ export default class MTPNetworker {
type: 'NETWORK_BAD_RESPONSE', type: 'NETWORK_BAD_RESPONSE',
transport: this.transport transport: this.transport
}; };
return promise.then((result) => { return promise.then((result) => {
if(!result || !result.byteLength) { if(!result?.byteLength) {
return Promise.reject(baseError); throw baseError;
} }
// this.debug && this.log.debug('sendEncryptedRequest: got response for:', message, [message.msg_id].concat(message.inner || []));
return result; return result;
}, (error) => { }, (error) => {
if(!error.message && !error.type) { if(!error.message && !error.type) {
error = Object.assign(baseError, { error = Object.assign(baseError, {
type: 'NETWORK_BAD_REQUEST', type: 'NETWORK_BAD_REQUEST',
originalError: error originalError: error
}); });
} }
return Promise.reject(error);
throw error;
}); });
/// #endif /// #endif
}); });

11
src/lib/mtproto/networkerFactory.ts

@ -12,9 +12,9 @@
import type { ConnectionStatusChange } from "./connectionStatus"; import type { ConnectionStatusChange } from "./connectionStatus";
import MTPNetworker from "./networker"; import MTPNetworker from "./networker";
import { InvokeApiOptions } from "../../types"; import { InvokeApiOptions } from "../../types";
import MTTransport from "./transports/transport";
import App from "../../config/app"; import App from "../../config/app";
import { MOUNT_CLASS_TO } from "../../config/debug"; import { MOUNT_CLASS_TO } from "../../config/debug";
import { indexOfAndSplice } from "../../helpers/array";
export class NetworkerFactory { export class NetworkerFactory {
private networkers: MTPNetworker[] = []; private networkers: MTPNetworker[] = [];
@ -25,19 +25,16 @@ export class NetworkerFactory {
public userAgent = navigator.userAgent; public userAgent = navigator.userAgent;
public removeNetworker(networker: MTPNetworker) { public removeNetworker(networker: MTPNetworker) {
const idx = this.networkers.indexOf(networker); indexOfAndSplice(this.networkers, networker);
if(idx !== -1) {
this.networkers.splice(idx, 1);
}
} }
public setUpdatesProcessor(callback: (obj: any) => void) { public setUpdatesProcessor(callback: (obj: any) => void) {
this.updatesProcessor = callback; this.updatesProcessor = callback;
} }
public getNetworker(dcId: number, authKey: Uint8Array, authKeyId: Uint8Array, serverSalt: Uint8Array, transport: MTTransport, options: InvokeApiOptions) { public getNetworker(dcId: number, authKey: Uint8Array, authKeyId: Uint8Array, serverSalt: Uint8Array, options: InvokeApiOptions) {
//console.log('NetworkerFactory: creating new instance of MTPNetworker:', dcId, options); //console.log('NetworkerFactory: creating new instance of MTPNetworker:', dcId, options);
const networker = new MTPNetworker(dcId, authKey, authKeyId, serverSalt, transport, options); const networker = new MTPNetworker(dcId, authKey, authKeyId, serverSalt, options);
this.networkers.push(networker); this.networkers.push(networker);
return networker; return networker;
} }

98
src/lib/mtproto/transports/controller.ts

@ -4,22 +4,110 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import { TransportType } from "../dcConfigurator"; import App from "../../../config/app";
import { deferredPromise } from "../../../helpers/cancellablePromise";
import EventListenerBase from "../../../helpers/eventListenerBase";
import { pause } from "../../../helpers/schedulers/pause";
import dcConfigurator, { TransportType } from "../dcConfigurator";
import type HTTP from "./http";
import type TcpObfuscated from "./tcpObfuscated";
import MTTransport from "./transport";
export class MTTransportController { export class MTTransportController extends EventListenerBase<{
change: (opened: MTTransportController['opened']) => void,
transport: (type: TransportType) => void
}> {
private opened: Map<TransportType, number>; private opened: Map<TransportType, number>;
private transports: {[k in TransportType]?: MTTransport};
private pinging: boolean;
constructor() { constructor() {
super(true);
this.opened = new Map(); this.opened = new Map();
/* this.addEventListener('change', (opened) => {
this.dispatchEvent('transport', opened.get('websocket') || !opened.get('https') ? 'websocket' : 'https');
}); */
this.addEventListener('change', (opened) => {
if(!opened.get('websocket')) {
this.waitForWebSocket();
}
});
setTimeout(() => {
this.waitForWebSocket();
}, 0);
} }
public setTransportOpened(type: TransportType, value: boolean) { public async pingTransports() {
let length = this.opened.get(type) || 0; const timeout = 2000;
const transports: {[k in TransportType]?: MTTransport} = this.transports = {
https: dcConfigurator.chooseServer(App.baseDcId, 'client', 'https', false),
websocket: dcConfigurator.chooseServer(App.baseDcId, 'client', 'websocket', false)
};
const httpPromise = deferredPromise<boolean>();
((this.transports.https as HTTP)._send(new Uint8Array(), 'no-cors') as any as Promise<any>)
.then(() => httpPromise.resolve(true), () => httpPromise.resolve(false));
setTimeout(() => httpPromise.resolve(false), timeout);
const websocketPromise = deferredPromise<boolean>();
const socket = transports.websocket as TcpObfuscated;
socket.setAutoReconnect(false);
socket.connection.addEventListener('close', () => websocketPromise.resolve(false), {once: true});
socket.connection.addEventListener('open', () => websocketPromise.resolve(true), {once: true});
setTimeout(() => websocketPromise.resolve(false), timeout);
const [isHttpAvailable, isWebSocketAvailable] = await Promise.all([httpPromise, websocketPromise]);
for(const transportType in transports) {
const transport = transports[transportType as TransportType];
transport.destroy();
}
return {
https: isHttpAvailable || this.opened.get('https') > 0,
websocket: isWebSocketAvailable || this.opened.get('websocket') > 0
};
}
public async waitForWebSocket() {
if(this.pinging) return;
this.pinging = true;
while(true) {
const {https, websocket} = await this.pingTransports();
if(https || websocket) {
this.dispatchEvent('transport', websocket || !https ? 'websocket' : 'https');
}
if(websocket) {
break;
}
await pause(10000);
}
this.pinging = false;
}
public setTransportValue(type: TransportType, value: boolean) {
let length = this.opened.get(type) || 0;
length += value ? 1 : -1; length += value ? 1 : -1;
this.opened.set(type, length); this.opened.set(type, length);
this.dispatchEvent('change', this.opened);
}
public setTransportOpened(type: TransportType) {
return this.setTransportValue(type, true);
}
public setTransportClosed(type: TransportType) {
return this.setTransportValue(type, false);
} }
} }
export default new MTTransportController(); const transportController = new MTTransportController();
export default transportController;

53
src/lib/mtproto/transports/http.ts

@ -6,9 +6,14 @@
import { pause } from '../../../helpers/schedulers/pause'; import { pause } from '../../../helpers/schedulers/pause';
import { DcId } from '../../../types'; import { DcId } from '../../../types';
import { logger } from '../../logger'; import { logger, LogTypes } from '../../logger';
import type MTPNetworker from '../networker'; import type MTPNetworker from '../networker';
import MTTransport from './transport'; import MTTransport from './transport';
import Modes from '../../../config/modes';
/// #if MTPROTO_AUTO
import transportController from './controller';
/// #endif
export default class HTTP implements MTTransport { export default class HTTP implements MTTransport {
public networker: MTPNetworker; public networker: MTPNetworker;
@ -21,13 +26,27 @@ export default class HTTP implements MTTransport {
}> = []; }> = [];
private releasing: boolean; private releasing: boolean;
public connected: boolean;
private destroyed: boolean;
private debug: boolean;
constructor(protected dcId: DcId, protected url: string, logSuffix: string) { constructor(protected dcId: DcId, protected url: string, logSuffix: string) {
this.log = logger(`HTTP-${dcId}` + logSuffix); this.debug = Modes.debug && false;
let logTypes = LogTypes.Error | LogTypes.Log;
if(this.debug) logTypes |= LogTypes.Debug;
this.log = logger(`HTTP-${dcId}` + logSuffix, logTypes);
this.log('constructor');
this.connected = false;
} }
private _send(body: Uint8Array) { public _send(body: Uint8Array, mode?: RequestMode) {
return fetch(this.url, {method: 'POST', body}).then(response => { this.debug && this.log.debug('-> body length to send:', body.length);
if(response.status !== 200) {
return fetch(this.url, {method: 'POST', body, mode}).then(response => {
if(response.status !== 200 && !mode) {
response.arrayBuffer().then(buffer => { response.arrayBuffer().then(buffer => {
this.log.error('not 200', this.log.error('not 200',
new TextDecoder("utf-8").decode(new Uint8Array(buffer))); new TextDecoder("utf-8").decode(new Uint8Array(buffer)));
@ -36,6 +55,8 @@ export default class HTTP implements MTTransport {
throw response; throw response;
} }
this.setConnected(true);
// * test resending by dropping random request // * test resending by dropping random request
// if(Math.random() > .5) { // if(Math.random() > .5) {
// throw 'asd'; // throw 'asd';
@ -44,9 +65,31 @@ export default class HTTP implements MTTransport {
return response.arrayBuffer().then(buffer => { return response.arrayBuffer().then(buffer => {
return new Uint8Array(buffer); return new Uint8Array(buffer);
}); });
}, (err) => {
this.setConnected(false);
throw err;
}); });
} }
private setConnected(connected: boolean) {
if(this.connected === connected || this.destroyed) {
return;
}
this.connected = connected;
/// #if MTPROTO_AUTO
transportController.setTransportValue('https', connected);
/// #endif
}
public destroy() {
this.setConnected(false);
this.destroyed = true;
this.pending.forEach(pending => pending.reject());
this.pending.length = 0;
}
public send(body: Uint8Array) { public send(body: Uint8Array) {
if(this.networker) { if(this.networker) {
return this._send(body); return this._send(body);

23
src/lib/mtproto/transports/tcpObfuscated.ts

@ -12,6 +12,10 @@ import MTTransport, { MTConnection, MTConnectionConstructable } from "./transpor
import intermediatePacketCodec from './intermediate'; import intermediatePacketCodec from './intermediate';
import { ConnectionStatus } from "../connectionStatus"; import { ConnectionStatus } from "../connectionStatus";
/// #if MTPROTO_AUTO
import transportController from "./controller";
/// #endif
export default class TcpObfuscated implements MTTransport { export default class TcpObfuscated implements MTTransport {
private codec = intermediatePacketCodec; private codec = intermediatePacketCodec;
private obfuscation = new Obfuscation(); private obfuscation = new Obfuscation();
@ -29,7 +33,7 @@ export default class TcpObfuscated implements MTTransport {
private log: ReturnType<typeof logger>; private log: ReturnType<typeof logger>;
public connected = false; public connected = false;
private lastCloseTime: number; private lastCloseTime: number;
private connection: MTConnection; public connection: MTConnection;
private autoReconnect = true; private autoReconnect = true;
private reconnectTimeout: number; private reconnectTimeout: number;
@ -53,6 +57,10 @@ export default class TcpObfuscated implements MTTransport {
private onOpen = () => { private onOpen = () => {
this.connected = true; this.connected = true;
/// #if MTPROTO_AUTO
transportController.setTransportOpened('websocket');
/// #endif
const initPayload = this.obfuscation.init(this.codec); const initPayload = this.obfuscation.init(this.codec);
this.connection.send(initPayload); this.connection.send(initPayload);
@ -136,6 +144,12 @@ export default class TcpObfuscated implements MTTransport {
}; };
public clear() { public clear() {
/// #if MTPROTO_AUTO
if(this.connected) {
transportController.setTransportClosed('websocket');
}
/// #endif
this.connected = false; this.connected = false;
if(this.connection) { if(this.connection) {
@ -183,6 +197,13 @@ export default class TcpObfuscated implements MTTransport {
public destroy() { public destroy() {
this.setAutoReconnect(false); this.setAutoReconnect(false);
this.close(); this.close();
this.pending.forEach(pending => {
if(pending.reject) {
pending.reject();
}
});
this.pending.length = 0;
} }
public close() { public close() {

4
src/lib/mtproto/transports/transport.ts

@ -10,6 +10,8 @@ import type MTPNetworker from "../networker";
export default interface MTTransport { export default interface MTTransport {
networker: MTPNetworker; networker: MTPNetworker;
send: (data: Uint8Array) => void; send: (data: Uint8Array) => void;
connected: boolean;
destroy: () => void;
} }
export interface MTConnection extends EventListenerBase<{ export interface MTConnection extends EventListenerBase<{
@ -17,7 +19,7 @@ export interface MTConnection extends EventListenerBase<{
message: (buffer: ArrayBuffer) => any, message: (buffer: ArrayBuffer) => any,
close: () => void, close: () => void,
}> { }> {
send: (data: Uint8Array) => void; send: MTTransport['send'];
close: () => void; close: () => void;
} }

8
src/lib/mtproto/transports/websocket.ts

@ -9,6 +9,9 @@ import Modes from '../../../config/modes';
import EventListenerBase from '../../../helpers/eventListenerBase'; import EventListenerBase from '../../../helpers/eventListenerBase';
import { MTConnection } from './transport'; import { MTConnection } from './transport';
// let closeSocketBefore = Date.now() + 30e3;
// let closeSocketAfter = Date.now() + 10e3;
export default class Socket extends EventListenerBase<{ export default class Socket extends EventListenerBase<{
open: () => void, open: () => void,
message: (buffer: ArrayBuffer) => any, message: (buffer: ArrayBuffer) => any,
@ -49,6 +52,11 @@ export default class Socket extends EventListenerBase<{
this.ws.addEventListener('close', this.handleClose); this.ws.addEventListener('close', this.handleClose);
this.ws.addEventListener('error', this.handleError); this.ws.addEventListener('error', this.handleError);
this.ws.addEventListener('message', this.handleMessage); this.ws.addEventListener('message', this.handleMessage);
// if(Date.now() < closeSocketBefore) {
// if(Date.now() >= closeSocketAfter) {
// this.ws.close();
// }
} }
public close() { public close() {

8
webpack.common.js

@ -17,11 +17,17 @@ if(devMode) {
console.log('DEVMODE IS ON!'); console.log('DEVMODE IS ON!');
} }
const MTPROTO_HTTP = false;
const MTPROTO_AUTO = true;
const opts = { const opts = {
MTPROTO_WORKER: true, MTPROTO_WORKER: true,
MTPROTO_SW: false, MTPROTO_SW: false,
MTPROTO_HTTP: false, MTPROTO_HTTP: MTPROTO_HTTP,
MTPROTO_HTTP_UPLOAD: false, MTPROTO_HTTP_UPLOAD: false,
MTPROTO_AUTO: MTPROTO_AUTO, // use HTTPS when WS is unavailable
MTPROTO_HAS_HTTP: MTPROTO_AUTO,
MTPROTO_HAS_WS: MTPROTO_AUTO || !MTPROTO_HTTP,
DEBUG: devMode, DEBUG: devMode,
version: 3, version: 3,
'ifdef-verbose': devMode, // add this for verbose output 'ifdef-verbose': devMode, // add this for verbose output

Loading…
Cancel
Save