diff --git a/components/02-connection.js b/components/02-connection.js index 4a3a5c9b..223e43c5 100644 --- a/components/02-connection.js +++ b/components/02-connection.js @@ -27,6 +27,12 @@ class SteamUserConnection extends SteamUserEnums { } this.emit('debug', `[${connPrefix}] Handling connection close`); + if (this._loggingOff) { + // We want to bail, so call _handleLogOff now (normally it's called at the end) + this._handleLogOff(EResult.NoConnection, 'Logged off'); + return; + } + this._cleanupClosedConnection(); if (!this.steamID) { @@ -37,7 +43,11 @@ class SteamUserConnection extends SteamUserEnums { this._ttlCache.add(`CM_DQ_${this._lastChosenCM.type}_${this._lastChosenCM.endpoint}`, 1, 1000 * 60 * 2); } - setTimeout(() => this._doConnection(), 1000); + // We save this timeout reference because it's possible that we handle connection close before we fully handle + // a logon response. In that case, we'll cancel this timeout when we handle the logon response. + // This isn't an issue in the reverse case, since a handled logon response will tear down the connection and + // remove all listeners. + this._reconnectForCloseDuringAuthTimeout = setTimeout(() => this._doConnection(), 1000); } else { // connection closed while we were connected; fire logoff this._handleLogOff(EResult.NoConnection, 'NoConnection'); @@ -45,8 +55,10 @@ class SteamUserConnection extends SteamUserEnums { } _cleanupClosedConnection() { - clearTimeout(this._logonTimeout); // cancel any queued reconnect attempt - clearTimeout(this._logonMsgTimeout); + this._connecting = false; + this._loggingOff = false; + + this._cancelReconnectTimers(); clearInterval(this._heartbeatInterval); this._connectionClosed = true; @@ -56,6 +68,12 @@ class SteamUserConnection extends SteamUserEnums { this._clearChangelistUpdateTimer(); } + _cancelReconnectTimers() { + clearTimeout(this._logonTimeout); + clearTimeout(this._logonMsgTimeout); + clearTimeout(this._reconnectForCloseDuringAuthTimeout); + } + _getProxyAgent() { if (this.options.socksProxy && this.options.httpProxy) { throw new Error('Cannot specify both socksProxy and httpProxy options'); diff --git a/components/09-logon.js b/components/09-logon.js index c579f9d7..fff653c6 100644 --- a/components/09-logon.js +++ b/components/09-logon.js @@ -43,15 +43,24 @@ class SteamUserLogon extends SteamUserMachineAuth { */ logOn(details) { // Delay the actual logon by one tick, so if users call logOn from the error event they won't get a crash because - // they appear to be already logged on (the steamID property is set to null only *after* the error event is emitted) + // they appear to be already logged on (the steamID property is set to null only *after* the error event is emitted). + // Go ahead and create the Error now, so that we'll have a useful stack trace if we need to throw it. + let alreadyLoggedOnError = new Error('Already logged on, cannot log on again'); + let alreadyConnectingError = new Error('Already attempting to log on, cannot log on again'); process.nextTick(async () => { if (this.steamID) { - throw new Error('Already logged on, cannot log on again'); + throw alreadyLoggedOnError; + } + + if (this._connecting) { + throw alreadyConnectingError; } this.steamID = null; + this._cancelReconnectTimers(); this._initProperties(); + this._connecting = true; this._loggingOff = false; if (details !== true) { @@ -301,9 +310,10 @@ class SteamUserLogon extends SteamUserMachineAuth { } if (++this._getCmListAttempts >= 10) { + this._cleanupClosedConnection(); this.emit('error', ex); } else { - setTimeout(() => this._doConnection(), 500); + setTimeout(() => this._doConnection(), 1000); } return; @@ -315,6 +325,7 @@ class SteamUserLogon extends SteamUserMachineAuth { } if (!cmListResponse.response || !cmListResponse.response.serverlist || Object.keys(cmListResponse.response.serverlist).length == 0) { + this._cleanupClosedConnection(); this.emit('error', new Error('No Steam servers available')); return; } @@ -481,7 +492,16 @@ class SteamUserLogon extends SteamUserMachineAuth { _enqueueLogonAttempt() { let timer = this._logonTimeoutDuration || 1000; this._logonTimeoutDuration = Math.min(timer * 2, 60000); // exponential backoff, max 1 minute + this.emit('debug', `Enqueueing login attempt in ${timer} ms`); this._logonTimeout = setTimeout(() => { + if (this.steamID || this._connecting) { + // Not sure why this happened, but we're already connected + let whyFail = this.steamID ? 'already connected' : 'already attempting to connect'; + this.emit('debug', `!! Attempted to fire queued login attempt, but we're ${whyFail}`); + return; + } + + this.emit('debug', 'Firing queued login attempt'); this.logOn(true); }, timer); } @@ -643,6 +663,7 @@ class SteamUserLogon extends SteamUserMachineAuth { this.steamID = null; } else { // Only emit "disconnected" if we were previously logged on + let wasLoggingOff = this._loggingOff; // remember this since our 'disconnected' event handler might reset it if (this.steamID) { this.emit('disconnected', result, msg); } @@ -650,7 +671,7 @@ class SteamUserLogon extends SteamUserMachineAuth { this._disconnect(true); // if we're manually relogging, or we got disconnected because steam went down, enqueue a reconnect - if (!this._loggingOff || this._relogging) { + if (!wasLoggingOff || this._relogging) { this._logonTimeout = setTimeout(() => { this.logOn(true); }, 1000); @@ -689,6 +710,11 @@ class SteamUserLogon extends SteamUserMachineAuth { async _handleLogOnResponse(body) { this.emit('debug', `Handle logon response (${EResult[body.eresult]})`); + this._connecting = false; + + clearTimeout(this._reconnectForCloseDuringAuthTimeout); + delete this._reconnectForCloseDuringAuthTimeout; + clearTimeout(this._logonMsgTimeout); delete this._logonMsgTimeout; diff --git a/components/chatroom.js b/components/chatroom.js index 5fdced81..9d07d1b7 100644 --- a/components/chatroom.js +++ b/components/chatroom.js @@ -294,7 +294,7 @@ class SteamChatRoomClient extends EventEmitter { } body.group_summary = processChatGroupSummary(body.group_summary, true); - body.user_chat_group_state = processUserChatGroupState(body.user_chat_group_state, true); + body.user_chat_group_state = body.user_chat_group_state ? processUserChatGroupState(body.user_chat_group_state, true) : null; body.banned = !!body.banned; body.invite_code = match[1]; resolve(body); diff --git a/components/connection_protocols/tcp.js b/components/connection_protocols/tcp.js index e30ff4a6..7f394c51 100644 --- a/components/connection_protocols/tcp.js +++ b/components/connection_protocols/tcp.js @@ -79,11 +79,13 @@ class TCPConnection extends BaseConnection { req.on('timeout', () => { connectionEstablished = true; + this.user._cleanupClosedConnection(); this.user.emit('error', new Error('Proxy connection timed out')); }); req.on('error', (err) => { connectionEstablished = true; + this.user._cleanupClosedConnection(); this.user.emit('error', err); }); } else { diff --git a/components/connection_protocols/websocket.js b/components/connection_protocols/websocket.js index 164fa40b..9831c40b 100644 --- a/components/connection_protocols/websocket.js +++ b/components/connection_protocols/websocket.js @@ -61,6 +61,7 @@ class WebSocketConnection extends BaseConnection { if (err.proxyConnecting || err.constructor.name == 'SocksClientError') { // This error happened while connecting to the proxy + this.user._cleanupClosedConnection(); this.user.emit('error', err); } else { this.user._handleConnectionClose(this); diff --git a/components/twofactor.js b/components/twofactor.js index 616dc6fd..bcf03d22 100644 --- a/components/twofactor.js +++ b/components/twofactor.js @@ -37,7 +37,7 @@ class SteamUserTwoFactor extends SteamUserTrading { /** * Finalize the process of enabling TOTP two-factor authentication - * @param {Buffer} secret - Your shared secret + * @param {Buffer|string} secret - Your shared secret * @param {string} activationCode - The activation code you got in your email * @param {function} [callback] - Called with a single Error argument, or null on success * @return {Promise} diff --git a/index.js b/index.js index ee11e0f1..a1468033 100644 --- a/index.js +++ b/index.js @@ -137,6 +137,9 @@ class SteamUser extends SteamUserTwoFactor { delete this._shouldAttemptRefreshTokenRenewal; delete this._loginSession; delete this._connectionClosed; + + clearTimeout(this._reconnectForCloseDuringAuthTimeout); + delete this._reconnectForCloseDuringAuthTimeout; } get packageName() { diff --git a/package.json b/package.json index 3a5a0e10..4f3d0e6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "steam-user", - "version": "5.0.4", + "version": "5.0.10", "description": "Steam client for Individual and AnonUser Steam account types", "keywords": [ "steam",