UNPKG

steam

Version:

Lets you interface with Steam without running an actual Steam client

319 lines (249 loc) 8.85 kB
var ByteBuffer = require('bytebuffer'); var EventEmitter = require('events').EventEmitter; var Steam = module.exports = require('steam-resources'); var schema = Steam.Internal; Steam._processProto = function(proto) { proto = proto.toRaw(false, true); (function deleteNulls(proto) { for (var field in proto) if (proto[field] == null) delete proto[field]; else if (typeof proto[field] == 'object') deleteNulls(proto[field]); })(proto); return proto; }; var EMsg = Steam.EMsg; var protoMask = 0x80000000; function SteamClient() { EventEmitter.call(this); } require('util').inherits(SteamClient, EventEmitter); // Methods Steam.servers = require('../servers').response.serverlist.map(function(server) { var [host, port] = server.split(':'); return { host, port }; }); SteamClient.prototype.connect = function() { this.disconnect(); this._jobs = {}; this._currentJobID = 0; this.sessionID = 0; var server = Steam.servers[Math.floor(Math.random() * Steam.servers.length)]; this.emit('debug', 'connecting to ' + server.host + ':' + server.port); this._connection = new (require('./connection'))(); this._connection.on('packet', this._netMsgReceived.bind(this)); this._connection.on('close', this._disconnected.bind(this)); var self = this; this._connection.on('error', function(err) { // it's ok, we'll reconnect after 'close' self.emit('debug', 'socket error: ' + err); }); this._connection.on('connect', function() { self.emit('debug', 'connected'); delete self._timeout; }); this._connection.on('end', function() { self.emit('debug', 'socket ended'); }); this._connection.setTimeout(1000, function() { self.emit('debug', 'socket timed out'); self._connection.destroy(); }); this._connection.connect(server.port, server.host); }; SteamClient.prototype.disconnect = function() { if (this._connection) { this._connection.destroy(); this._connection.removeAllListeners(); delete this._connection; if (this.loggedOn) { this.loggedOn = false; clearInterval(this._heartBeatFunc); } this.connected = false; } else if (this._scheduledConnection) { // there was an error and we're currently waiting clearTimeout(this._scheduledConnection); delete this._scheduledConnection; } }; SteamClient.prototype._send = function(header, body, callback) { if (callback) { var sourceJobID = ++this._currentJobID; this._jobs[sourceJobID] = callback; } if (header.msg == EMsg.ChannelEncryptResponse) { header.sourceJobID = sourceJobID; header = new schema.MsgHdr(header); } else if (header.proto) { header.proto.client_sessionid = this.sessionID; header.proto.steamid = this.steamID; header.proto.jobid_source = sourceJobID; header = new schema.MsgHdrProtoBuf(header); } else { header.steamID = this.steamID; header.sessionID = this.sessionID; header.sourceJobID = sourceJobID; header = new schema.ExtendedClientMsgHdr(header); } this._connection.send(Buffer.concat([header.toBuffer(), body])); }; SteamClient.prototype.send = function(header, body, callback) { // ignore any target job ID if (header.proto) delete header.proto.jobid_target; else delete header.targetJobID; this._send(header, body, callback); }; SteamClient.prototype._netMsgReceived = function(data) { var rawEMsg = data.readUInt32LE(0); var eMsg = rawEMsg & ~protoMask; data = ByteBuffer.wrap(data, ByteBuffer.LITTLE_ENDIAN); var header, sourceJobID, targetJobID; if (eMsg == EMsg.ChannelEncryptRequest || eMsg == EMsg.ChannelEncryptResult) { header = schema.MsgHdr.decode(data); sourceJobID = header.sourceJobID; targetJobID = header.targetJobID; } else if (rawEMsg & protoMask) { header = schema.MsgHdrProtoBuf.decode(data); header.proto = Steam._processProto(header.proto); if (!this.sessionID && header.headerLength > 0) { this.sessionID = header.proto.client_sessionid; this.steamID = header.proto.steamid; } sourceJobID = header.proto.jobid_source; targetJobID = header.proto.jobid_target; } else { header = schema.ExtendedClientMsgHdr.decode(data); sourceJobID = header.sourceJobID; targetJobID = header.targetJobID; } var body = data.toBuffer(); if (eMsg in handlers) handlers[header.msg].call(this, body); if (sourceJobID != '18446744073709551615') { var callback = function(header, body, callback) { if (header.proto) header.proto.jobid_target = sourceJobID; else header.targetJobID = sourceJobID; this._send(header, body, callback); }.bind(this); } if (targetJobID in this._jobs) this._jobs[targetJobID](header, body, callback); else this.emit('message', header, body, callback); }; SteamClient.prototype._disconnected = function(had_error) { this.emit('debug', 'socket closed' + (had_error ? ' with an error' : '')); delete this._connection; if (this.connected) { if (this.loggedOn) { this.emit('debug', 'unexpected disconnection'); this.loggedOn = false; clearInterval(this._heartBeatFunc); } this.connected = false; this.emit('error', new Error('Disconnected')); return; } if (!had_error) { this.connect(); return; } var timeout = this._timeout || 1; this.emit('debug', 'waiting ' + timeout + ' secs'); this._scheduledConnection = setTimeout(function() { delete this._scheduledConnection; this.connect(); }.bind(this), timeout * 1000); this._timeout = timeout * 2; }; // Handlers var handlers = {}; handlers[EMsg.ChannelEncryptRequest] = function(data) { // assume server isn't dead this._connection.setTimeout(0); // var encRequest = schema.MsgChannelEncryptRequest.decode(data); this.emit('debug', 'encrypt request'); var sessionKey = require('steam-crypto').generateSessionKey(); this._tempSessionKey = sessionKey.plain; var keyCrc = require('buffer-crc32').signed(sessionKey.encrypted); var encResp = new schema.MsgChannelEncryptResponse().encode(); var body = new ByteBuffer(encResp.limit + 128 + 4 + 4, ByteBuffer.LITTLE_ENDIAN); // key, crc, trailer body.append(encResp); body.append(sessionKey.encrypted); body.writeInt32(keyCrc); body.writeUint32(0); // TODO: check if the trailer is required body.flip(); this.send({ msg: EMsg.ChannelEncryptResponse }, body.toBuffer()); }; handlers[EMsg.ChannelEncryptResult] = function(data) { var encResult = schema.MsgChannelEncryptResult.decode(data); if (encResult.result == Steam.EResult.OK) { this._connection.sessionKey = this._tempSessionKey; } else { this.emit('error', new Error("Encryption fail: " + encResult.result)); return; } this.connected = true; this.emit('connected'); }; handlers[EMsg.Multi] = function(data) { var msgMulti = schema.CMsgMulti.decode(data); var payload = msgMulti.message_body.toBuffer(); if (msgMulti.size_unzipped) { var zip = new (require('adm-zip'))(payload); payload = zip.readFile('z'); } // stop handling if user disconnected while (payload.length && this.connected) { var subSize = payload.readUInt32LE(0); this._netMsgReceived(payload.slice(4, 4 + subSize)); payload = payload.slice(4 + subSize); } }; handlers[EMsg.ClientLogOnResponse] = function(data) { var logonResp = schema.CMsgClientLogonResponse.decode(data); var eresult = logonResp.eresult; if (eresult == Steam.EResult.OK) { var hbDelay = logonResp.out_of_game_heartbeat_seconds; this._heartBeatFunc = setInterval(function() { this.send({ msg: EMsg.ClientHeartBeat, proto: {} }, new schema.CMsgClientHeartBeat().toBuffer()); }.bind(this), hbDelay * 1000); this.loggedOn = true; } this.emit('logOnResponse', Steam._processProto(logonResp)); }; handlers[EMsg.ClientLoggedOff] = function(data) { this.loggedOn = false; clearInterval(this._heartBeatFunc); var eresult = schema.CMsgClientLoggedOff.decode(data).eresult; this.emit('loggedOff', eresult); }; handlers[EMsg.ClientCMList] = function(data) { var list = schema.CMsgClientCMList.decode(data); var servers = list.cm_addresses.map(function(number, index) { var buf = new Buffer(4); buf.writeUInt32BE(number, 0); return { host: [].join.call(buf, '.'), port: list.cm_ports[index] }; }); this.emit('servers', servers); Steam.servers = servers; }; Steam.SteamClient = SteamClient; require('./handlers/friends'); require('./handlers/game_coordinator'); require('./handlers/rich_presence'); require('./handlers/trading'); require('./handlers/unified_messages'); require('./handlers/user');