diff --git a/examples/disable_twofactor.js b/examples/disable_twofactor.js index 389ca93..e03411b 100644 --- a/examples/disable_twofactor.js +++ b/examples/disable_twofactor.js @@ -1,73 +1,81 @@ // If you aren't running this script inside of the repository, replace the following line with: // const SteamCommunity = require('steamcommunity'); const SteamCommunity = require('../index.js'); +const SteamSession = require('steam-session'); const ReadLine = require('readline'); +let g_AbortPromptFunc = null; + let community = new SteamCommunity(); -let rl = ReadLine.createInterface({ - input: process.stdin, - output: process.stdout -}); -rl.question('Username: ', (accountName) => { - rl.question('Password: ', (password) => { - rl.question('Two-Factor Auth Code: ', (authCode) =>{ - rl.question('Revocation Code: R', (rCode) => { - doLogin(accountName, password, authCode, '', rCode); - }); - }); +main(); +async function main() { + let accountName = await promptAsync('Username: '); + let password = await promptAsync('Password (hidden): ', true); + + // Create a LoginSession for us to use to attempt to log into steam + let session = new SteamSession.LoginSession(SteamSession.EAuthTokenPlatformType.MobileApp); + + // Go ahead and attach our event handlers before we do anything else. + session.on('authenticated', async () => { + abortPrompt(); + + let accessToken = session.accessToken; + let cookies = await session.getWebCookies(); + + community.setCookies(cookies); + community.setMobileAppAccessToken(accessToken); + + doRevoke(); }); -}); -function doLogin(accountName, password, authCode, captcha, rCode) { - community.login({ - accountName: accountName, - password: password, - twoFactorCode: authCode, - captcha: captcha - }, (err, sessionID, cookies, steamguard) => { - if (err) { - if (err.message == 'SteamGuard') { - console.log('This account does not have two-factor authentication enabled.'); - process.exit(); - return; + session.on('timeout', () => { + abortPrompt(); + console.log('This login attempt has timed out.'); + }); + + session.on('error', (err) => { + abortPrompt(); + + // This should ordinarily not happen. This only happens in case there's some kind of unexpected error while + // polling, e.g. the network connection goes down or Steam chokes on something. + + console.log(`ERROR: This login attempt has failed! ${err.message}`); + }); + + // Start our login attempt + let startResult = await session.startWithCredentials({accountName, password}); + if (startResult.actionRequired) { + // Some Steam Guard action is required. We only care about email and device codes; in theory an + // EmailConfirmation and/or DeviceConfirmation action could be possible, but we're just going to ignore those. + // If the user does receive a confirmation and accepts it, LoginSession will detect and handle that automatically. + // The only consequence of ignoring it here is that we don't print a message to the user indicating that they + // could accept an email or device confirmation. + + let codeActionTypes = [SteamSession.EAuthSessionGuardType.EmailCode, SteamSession.EAuthSessionGuardType.DeviceCode]; + let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type)); + if (codeAction) { + if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) { + // We wouldn't expect this to happen since we're trying to disable 2FA, but just in case... + console.log(`A code has been sent to your email address at ${codeAction.detail}.`); + } else { + console.log('You need to provide a Steam Guard Mobile Authenticator code.'); } - if (err.message == 'CAPTCHA') { - console.log(err.captchaurl); - rl.question('CAPTCHA: ', (captchaInput) => { - doLogin(accountName, password, authCode, captchaInput); - }); - - return; + let code = await promptAsync('Code: '); + if (code) { + await session.submitSteamGuardCode(code); } - console.log(err); - process.exit(); - return; + // If we fall through here without submitting a Steam Guard code, that means one of two things: + // 1. The user pressed enter without providing a code, in which case the script will simply exit + // 2. The user approved a device/email confirmation, in which case 'authenticated' was emitted and the prompt was canceled } - - console.log('Logged on!'); - - if (community.mobileAccessToken) { - // If we already have a mobile access token, we don't need to prompt for one. - doRevoke(rCode); - return; - } - - console.log('You need to provide a mobile app access token to continue.'); - console.log('You can generate one using steam-session (https://www.npmjs.com/package/steam-session).'); - console.log('The access token needs to be generated using EAuthTokenPlatformType.MobileApp.'); - console.log('Make sure you provide an *ACCESS* token, not a refresh token.'); - - rl.question('Access Token: ', (accessToken) => { - community.setMobileAppAccessToken(accessToken); - doRevoke(rCode); - }); - }); + } } -function doRevoke(rCode) { +async function doRevoke() { + let rCode = await promptAsync('Revocation Code: R'); community.disableTwoFactor('R' + rCode, (err) => { if (err) { console.log(err); @@ -79,3 +87,45 @@ function doRevoke(rCode) { process.exit(); }); } + +// Nothing interesting below here, just code for prompting for input from the console. + +function promptAsync(question, sensitiveInput = false) { + return new Promise((resolve) => { + let rl = ReadLine.createInterface({ + input: process.stdin, + output: sensitiveInput ? null : process.stdout, + terminal: true + }); + + g_AbortPromptFunc = () => { + rl.close(); + resolve(''); + }; + + if (sensitiveInput) { + // We have to write the question manually if we didn't give readline an output stream + process.stdout.write(question); + } + + rl.question(question, (result) => { + if (sensitiveInput) { + // We have to manually print a newline + process.stdout.write('\n'); + } + + g_AbortPromptFunc = null; + rl.close(); + resolve(result); + }); + }); +} + +function abortPrompt() { + if (!g_AbortPromptFunc) { + return; + } + + g_AbortPromptFunc(); + process.stdout.write('\n'); +} diff --git a/examples/enable_twofactor.js b/examples/enable_twofactor.js index 9f95942..a739a02 100644 --- a/examples/enable_twofactor.js +++ b/examples/enable_twofactor.js @@ -1,78 +1,80 @@ // If you aren't running this script inside of the repository, replace the following line with: // const SteamCommunity = require('steamcommunity'); const SteamCommunity = require('../index.js'); +const SteamSession = require('steam-session'); const ReadLine = require('readline'); const FS = require('fs'); const EResult = SteamCommunity.EResult; +let g_AbortPromptFunc = null; + let community = new SteamCommunity(); -let rl = ReadLine.createInterface({ - input: process.stdin, - output: process.stdout -}); -rl.question('Username: ', (accountName) => { - rl.question('Password: ', (password) => { - doLogin(accountName, password); +main(); +async function main() { + let accountName = await promptAsync('Username: '); + let password = await promptAsync('Password (hidden): ', true); + + // Create a LoginSession for us to use to attempt to log into steam + let session = new SteamSession.LoginSession(SteamSession.EAuthTokenPlatformType.MobileApp); + + // Go ahead and attach our event handlers before we do anything else. + session.on('authenticated', async () => { + abortPrompt(); + + let accessToken = session.accessToken; + let cookies = await session.getWebCookies(); + + community.setCookies(cookies); + community.setMobileAppAccessToken(accessToken); + + doSetup(); }); -}); -function doLogin(accountName, password, authCode, captcha) { - community.login({ - accountName: accountName, - password: password, - authCode: authCode, - captcha: captcha - }, (err, sessionID, cookies, steamguard) => { - if (err) { - if (err.message == 'SteamGuardMobile') { - console.log('This account already has two-factor authentication enabled.'); - process.exit(); - return; - } - - if (err.message == 'SteamGuard') { - console.log(`An email has been sent to your address at ${err.emaildomain}`); - rl.question('Steam Guard Code: ', (code) => { - doLogin(accountName, password, code); - }); - - return; - } - - if (err.message == 'CAPTCHA') { - console.log(err.captchaurl); - rl.question('CAPTCHA: ', (captchaInput) => { - doLogin(accountName, password, authCode, captchaInput); - }); - - return; - } - - console.log(err); - process.exit(); - return; - } - - console.log('Logged on!'); - - if (community.mobileAccessToken) { - // If we already have a mobile access token, we don't need to prompt for one. - doSetup(); - return; - } - - console.log('You need to provide a mobile app access token to continue.'); - console.log('You can generate one using steam-session (https://www.npmjs.com/package/steam-session).'); - console.log('The access token needs to be generated using EAuthTokenPlatformType.MobileApp.'); - console.log('Make sure you provide an *ACCESS* token, not a refresh token.'); - - rl.question('Access Token: ', (accessToken) => { - community.setMobileAppAccessToken(accessToken); - doSetup(); - }); + session.on('timeout', () => { + abortPrompt(); + console.log('This login attempt has timed out.'); }); + + session.on('error', (err) => { + abortPrompt(); + + // This should ordinarily not happen. This only happens in case there's some kind of unexpected error while + // polling, e.g. the network connection goes down or Steam chokes on something. + + console.log(`ERROR: This login attempt has failed! ${err.message}`); + }); + + // Start our login attempt + let startResult = await session.startWithCredentials({accountName, password}); + if (startResult.actionRequired) { + // Some Steam Guard action is required. We only care about email and device codes; in theory an + // EmailConfirmation and/or DeviceConfirmation action could be possible, but we're just going to ignore those. + // If the user does receive a confirmation and accepts it, LoginSession will detect and handle that automatically. + // The only consequence of ignoring it here is that we don't print a message to the user indicating that they + // could accept an email or device confirmation. + + let codeActionTypes = [SteamSession.EAuthSessionGuardType.EmailCode, SteamSession.EAuthSessionGuardType.DeviceCode]; + let codeAction = startResult.validActions.find(action => codeActionTypes.includes(action.type)); + if (codeAction) { + if (codeAction.type == SteamSession.EAuthSessionGuardType.EmailCode) { + console.log(`A code has been sent to your email address at ${codeAction.detail}.`); + } else { + // We wouldn't expect this to happen since we're trying to enable 2FA, but just in case... + console.log('You need to provide a Steam Guard Mobile Authenticator code.'); + } + + let code = await promptAsync('Code: '); + if (code) { + await session.submitSteamGuardCode(code); + } + + // If we fall through here without submitting a Steam Guard code, that means one of two things: + // 1. The user pressed enter without providing a code, in which case the script will simply exit + // 2. The user approved a device/email confirmation, in which case 'authenticated' was emitted and the prompt was canceled + } + } } function doSetup() { @@ -110,22 +112,67 @@ function doSetup() { }); } -function promptActivationCode(response) { - rl.question('SMS Code: ', (smsCode) => { - community.finalizeTwoFactor(response.shared_secret, smsCode, (err) => { - if (err) { - if (err.message == 'Invalid activation code') { - console.log(err); - promptActivationCode(response); - return; - } +async function promptActivationCode(response) { + if (response.phone_number_hint) { + console.log(`A code has been sent to your phone ending in ${response.phone_number_hint}.`); + } + let smsCode = await promptAsync('SMS Code: '); + community.finalizeTwoFactor(response.shared_secret, smsCode, (err) => { + if (err) { + if (err.message == 'Invalid activation code') { console.log(err); - } else { - console.log('Two-factor authentication enabled!'); + promptActivationCode(response); + return; } - process.exit(); + console.log(err); + } else { + console.log('Two-factor authentication enabled!'); + } + + process.exit(); + }); +} + +// Nothing interesting below here, just code for prompting for input from the console. + +function promptAsync(question, sensitiveInput = false) { + return new Promise((resolve) => { + let rl = ReadLine.createInterface({ + input: process.stdin, + output: sensitiveInput ? null : process.stdout, + terminal: true + }); + + g_AbortPromptFunc = () => { + rl.close(); + resolve(''); + }; + + if (sensitiveInput) { + // We have to write the question manually if we didn't give readline an output stream + process.stdout.write(question); + } + + rl.question(question, (result) => { + if (sensitiveInput) { + // We have to manually print a newline + process.stdout.write('\n'); + } + + g_AbortPromptFunc = null; + rl.close(); + resolve(result); }); }); } + +function abortPrompt() { + if (!g_AbortPromptFunc) { + return; + } + + g_AbortPromptFunc(); + process.stdout.write('\n'); +} diff --git a/package.json b/package.json index 82f4872..399f69a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,9 @@ "steamid": "^1.1.3", "xml2js": "^0.4.22" }, + "devDependencies": { + "steam-session": "^1.2.3" + }, "engines": { "node": ">=4.0.0" }