UNPKG

steam-client

Version:

SteamClient from node-steam with proper versioning and more features

188 lines (157 loc) 5.35 kB
var SteamCrypto = require('@doctormckay/steam-crypto'); var Socket = require('net').Socket; var HTTP = require('http'); var URL = require('url'); module.exports = TCPConnection; const MAGIC = 'VT01'; require('util').inherits(TCPConnection, require('events').EventEmitter); /* We are expected to implement the following events: - packet (a complete message from Steam was received) - close (the connection was closed) - error (a fatal error occurred, but close will also be emitted) - connect (a successful connection was established) - end (emitted after close) - timeout (emitted after setTimeout elapses) We are expected to implement the following methods: - end() (request a disconnect) - destroy() (immediately kill the connection, without notifying the other side) - connect(options) (start connecting to the remote server) - port (the port number of the remote host) - host (the hostname or IP address of the remote host) - localAddress (the local IP we want to bind to, optional) - localPort (the local port we want to bind to, optional) - send(buffer) (send a raw buffer through the connection, encrypting if necessary) - setTimeout(ms[, callback]) (identical to Socket's setTimeout) */ /** * @constructor */ function TCPConnection() { // We don't really need to set anything up here } TCPConnection.prototype.connect = function(options, callback) { if (options.httpProxy) { var self = this; var url = URL.parse(options.httpProxy); url.method = 'CONNECT'; url.path = options.host + ':' + options.port; url.localAddress = options.localAddress; url.localPort = options.localPort; if (url.auth) { url.headers = {"Proxy-Authorization": "Basic " + (new Buffer(url.auth, 'utf8')).toString('base64')}; delete url.auth; } var connectionEstablished = false; var req = this._request = HTTP.request(url); req.end(); req.setTimeout(options.proxyTimeout || 5000); req.on('connect', function(res, socket) { if (connectionEstablished) { socket.end(); return; } connectionEstablished = true; req.setTimeout(0); if (res.statusCode != 200) { callback(new Error("HTTP CONNECT " + res.statusCode + " " + res.statusMessage)); return; } self._stream = socket; self._setupStream(); callback(); self.emit('connect'); }); req.on('timeout', function() { connectionEstablished = true; callback(new Error("Proxy connection timed out")); }); req.on('error', function() { if (!connectionEstablished) { callback.apply(self, Array.prototype.slice.call(arguments)); return; } self.emit.apply(self, ['error'].concat(Array.prototype.slice.call(arguments))); connectionEstablished = true; }); } else { this._stream = new Socket(); this._setupStream(); callback(); this._stream.connect(options); } }; TCPConnection.prototype._setupStream = function() { var self = this; this._stream.on('readable', this._readPacket.bind(this)); this._stream.on('close', function() { self.emit.apply(self, ['close'].concat(Array.prototype.slice.call(arguments))); }); this._stream.on('error', function() { self.emit.apply(self, ['error'].concat(Array.prototype.slice.call(arguments))); }); this._stream.on('connect', function() { self.emit.apply(self, ['connect'].concat(Array.prototype.slice.call(arguments))); }); this._stream.on('end', function() { self.emit.apply(self, ['end'].concat(Array.prototype.slice.call(arguments))); }); this._stream.on('timeout', function() { self.emit.apply(self, ['timeout'].concat(Array.prototype.slice.call(arguments))); }); this.setTimeout = this._stream.setTimeout.bind(this._stream); }; TCPConnection.prototype.end = function() { if (this._stream) { this._stream.end(); } if (this._request) { this._request.abort(); } }; TCPConnection.prototype.destroy = function() { if (this._stream) { this._stream.destroy(); } }; /** * Send data over the connection. * @param {Buffer} data */ TCPConnection.prototype.send = function(data) { // encrypt if (this.sessionKey) { if (this.useHmac) { data = SteamCrypto.symmetricEncryptWithHmacIv(data, this.sessionKey); } else { data = SteamCrypto.symmetricEncrypt(data, this.sessionKey); } } var buffer = new Buffer(4 + 4 + data.length); buffer.writeUInt32LE(data.length, 0); buffer.write(MAGIC, 4); data.copy(buffer, 8); this._stream.write(buffer); }; TCPConnection.prototype._readPacket = function() { if (!this._packetLen) { var header = this._stream.read(8); if (!header) { return; } this._packetLen = header.readUInt32LE(0); if (header.slice(4).toString('ascii') != MAGIC) { this.emit('error', new Error('Bad magic')); this.end(); return; } } var packet = this._stream.read(this._packetLen); if (!packet) { this.emit('debug', 'incomplete packet'); return; } delete this._packetLen; // decrypt if (this.sessionKey) { try { packet = SteamCrypto.symmetricDecrypt(packet, this.sessionKey, this.useHmac); } catch (ex) { this.emit('encryptionError', ex); return; } } this.emit('packet', packet); // keep reading until there's nothing left this._readPacket(); };