steam-user
Version:
Steam client for Individual and AnonUser Steam account types
253 lines (216 loc) • 6.36 kB
JavaScript
const HTTP = require('http');
const Socket = require('net').Socket;
const SteamCrypto = require('@doctormckay/steam-crypto');
const {URL} = require('url');
const BaseConnection = require('./base.js');
const EResult = require('../../enums/EResult');
const MAGIC = 'VT01';
/**
* @typedef CmServer
* @property {string} endpoint
* @property {string} legacy_endpoint
* @property {string} type
* @property {string} dc
* @property {string} realm
* @property {string} load
* @property {string} wtd_load
*/
class TCPConnection extends BaseConnection {
/**
* Create a new TCP connection, and connect
* @param {SteamUser} user
* @param {CmServer} chosenServer
* @constructor
*/
constructor(user, chosenServer) {
super(user);
this.connectionType = 'TCP';
this.sessionKey = null;
this._debug('Connecting to TCP CM: ' + chosenServer.endpoint);
let cmParts = chosenServer.endpoint.split(':');
let cmHost = cmParts[0];
let cmPort = parseInt(cmParts[1], 10);
if (user.options.httpProxy) {
let url = new URL(user.options.httpProxy);
let prox = {
protocol: url.protocol,
host: url.hostname,
port: url.port,
method: 'CONNECT',
path: chosenServer.endpoint,
localAddress: user.options.localAddress,
localPort: user.options.localPort
};
if (url.username) {
prox.headers = {
'Proxy-Authorization': `Basic ${(Buffer.from(`${url.username}:${url.password || ''}`, 'utf8')).toString('base64')}`
};
}
let connectionEstablished = false;
let req = HTTP.request(prox);
req.end();
req.setTimeout(user.options.proxyTimeout || 5000);
req.on('connect', (res, socket) => {
if (connectionEstablished) {
// somehow we're already connected, or we aborted
socket.end();
return;
}
connectionEstablished = true;
req.setTimeout(0); // disable timeout
if (res.statusCode != 200) {
this._fatal(new Error(`Proxy HTTP CONNECT ${res.statusCode} ${res.statusMessage}`));
return;
}
this.stream = socket;
this._setupStream();
});
req.on('timeout', () => {
connectionEstablished = true;
this.user._cleanupClosedConnection();
this._fatal(new Error('Proxy connection timed out'));
});
req.on('error', (err) => {
connectionEstablished = true;
this.user._cleanupClosedConnection();
this._fatal(err);
});
} else {
let socket = new Socket();
this.stream = socket;
this._setupStream();
socket.connect({
port: cmPort,
host: cmHost,
localAddress: user.options.localAddress,
localPort: user.options.localPort
});
this.stream.setTimeout(this.user._connectTimeout);
}
}
/**
* Set up the stream with event handlers
* @private
*/
_setupStream() {
this.stream.on('readable', this._readMessage.bind(this));
this.stream.on('error', (err) => this._debug('TCP connection error: ' + err.message)); // "close" will be emitted and we'll reconnect
this.stream.on('end', () => this._debug('TCP connection ended'));
this.stream.on('close', () => this.user._handleConnectionClose(this));
this.stream.on('connect', () => {
this._debug('TCP connection established');
this.stream.setTimeout(Math.min(this.user._connectTimeout, 2000)); // give the CM max 2 seconds to send ChannelEncryptRequest
});
this.stream.on('timeout', () => {
this._debug('TCP connection timed out');
this.user._connectTimeout = Math.min(this.user._connectTimeout * 2, 10000); // 10 seconds max
this.end(true);
this.user._doConnection();
});
}
/**
* End the connection
* @param {boolean} [andIgnore=false] - Pass true to also ignore all further events from this connection
*/
end(andIgnore) {
if (this.stream) {
this._debug('Ending connection' + (andIgnore ? ' and removing all listeners' : ''));
if (andIgnore) {
this.removeAllListeners();
this.stream.removeAllListeners();
this.stream.on('error', () => {
});
}
this.stream.end();
if (andIgnore) {
this.stream.destroy();
}
} else {
this._debug('We wanted to end connection, but there is no stream');
}
}
/**
* Send data over the connection
* @param {Buffer} data
*/
send(data) {
if (this.sessionKey) {
data = SteamCrypto.symmetricEncryptWithHmacIv(data, this.sessionKey);
}
if (!this.stream) {
this._debug('Tried to send message, but there is no stream');
return;
}
let buf = Buffer.alloc(4 + 4 + data.length);
buf.writeUInt32LE(data.length, 0);
buf.write(MAGIC, 4);
data.copy(buf, 8);
try {
this.stream.write(buf);
} catch (error) {
this._debug('Error writing to socket: ' + error.message);
this._fatal(error);
}
}
/**
* Try to read a message from the socket
* @private
*/
_readMessage() {
if (!this._messageLength) {
// We are not in the middle of a message, so the next thing on the wire should be a header
let header = this.stream.read(8);
if (!header) {
return; // maybe we should tear down the connection here
}
this._messageLength = header.readUInt32LE(0);
if (header.slice(4).toString('ascii') != MAGIC) {
// We definitely need to tear down the connection here
this._fatal(new Error('Connection out of sync'));
return;
}
}
if (!this.stream) {
this._debug('Tried to read message, but there is no stream');
return;
}
let message;
try {
message = this.stream.read(this._messageLength);
} catch (error) {
this._debug('Error reading from socket: ' + error.message);
this._fatal(error);
return;
}
if (!message) {
this._debug('Got incomplete message; expecting ' + this._messageLength + ' more bytes');
return;
}
delete this._messageLength;
if (this.sessionKey) {
try {
message = SteamCrypto.symmetricDecrypt(message, this.sessionKey, true);
} catch (ex) {
this._fatal(new Error('Encrypted message authentication failed'));
return;
}
}
// noinspection JSAccessibilityCheck
this.user._handleNetMessage(message, this);
this._readMessage();
}
/**
* There was a fatal transport error
* @param {Error} err
* @private
*/
_fatal(err) {
if (!err.message.startsWith('Proxy')) {
err.eresult = EResult.NoConnection;
}
this.user.emit('error', err);
// noinspection JSAccessibilityCheck
this.user._disconnect(true);
}
}
module.exports = TCPConnection;