diff --git a/index.d.ts b/index.d.ts index b63ecdd..e1fde3c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -108,6 +108,14 @@ declare module 'prismarine-auth' { refreshAfter: string } + export interface ProxyOptions { + host: string + port: number + username?: string + password?: string + type?: string + } + export interface MicrosoftAuthFlowOptions { // If using Azure auth, specify an custom object to pass to MSAL msalConfig?: object @@ -118,6 +126,8 @@ declare module 'prismarine-auth' { flow: 'live' | 'msal' | 'sisu' // Reset the cache and obtain fresh tokens for everything forceRefresh?: boolean + // Proxy configuration + proxy?: ProxyOptions } export enum Titles { diff --git a/package.json b/package.json index b288dfe..dfabf00 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,9 @@ "@azure/msal-node": "^2.0.2", "@xboxreplay/xboxlive-auth": "^3.3.3", "debug": "^4.3.3", + "fetch-socks": "^1.3.0", "smart-buffer": "^4.1.0", + "undici": "^6.0.0", "uuid-1345": "^1.0.2" } } diff --git a/src/MicrosoftAuthFlow.js b/src/MicrosoftAuthFlow.js index ecdb87b..c0475a8 100644 --- a/src/MicrosoftAuthFlow.js +++ b/src/MicrosoftAuthFlow.js @@ -2,6 +2,8 @@ const fs = require('fs') const path = require('path') const crypto = require('crypto') const debug = require('debug')('prismarine-auth') +const { socksDispatcher } = require('fetch-socks') +const { ProxyAgent } = require('undici') const Titles = require('./common/Titles') const { createHash } = require('./common/Util') @@ -16,6 +18,23 @@ const BedrockTokenManager = require('./TokenManagers/MinecraftBedrockTokenManage const PlayfabTokenManager = require('./TokenManagers/PlayfabTokenManager') const MinecraftServicesTokenManager = require('./TokenManagers/MinecraftBedrockServicesManager') +function createProxyFetch (proxy) { + if (proxy.type === 'socks5') { + return (url, options = {}) => fetch(url, { + ...options, + dispatcher: socksDispatcher({ + type: 5, + host: proxy.host, + port: proxy.port, + userId: proxy.username, + password: proxy.password + }) + }) + } + // otherwise use http proxy undici + return (url, options = {}) => fetch(url, { ...options, dispatcher: new ProxyAgent(`http://${proxy.username}:${proxy.password}@${proxy.host}:${proxy.port}`) }) +} + async function retry (methodFn, beforeRetry, times) { while (times--) { if (times !== 0) { @@ -37,6 +56,7 @@ class MicrosoftAuthFlow { throw new Error("Missing 'flow' argument in options. See docs for more information.") } this.options = options || { flow: 'live', authTitle: Titles.MinecraftNintendoSwitch } + this.fetch = this.options.proxy ? createProxyFetch(this.options.proxy) : fetch this.initTokenManagers(username, cache, options?.forceRefresh) this.codeCallback = codeCallback } @@ -71,7 +91,7 @@ class MicrosoftAuthFlow { if (this.options.flow === 'live' || this.options.flow === 'sisu') { if (!this.options.authTitle) throw new Error(`Please specify an "authTitle" in Authflow constructor when using ${this.options.flow} flow`) - this.msa = new LiveTokenManager(this.options.authTitle, ['service::user.auth.xboxlive.com::MBI_SSL'], cache({ cacheName: this.options.flow, username })) + this.msa = new LiveTokenManager(this.options.authTitle, ['service::user.auth.xboxlive.com::MBI_SSL'], cache({ cacheName: this.options.flow, username }), this.fetch) this.doTitleAuth = true } else if (this.options.flow === 'msal') { let config = this.options.msalConfig @@ -86,11 +106,11 @@ class MicrosoftAuthFlow { } const keyPair = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }) - this.xbl = new XboxTokenManager(keyPair, cache({ cacheName: 'xbl', username })) - this.mba = new BedrockTokenManager(cache({ cacheName: 'bed', username })) - this.mca = new JavaTokenManager(cache({ cacheName: 'mca', username })) - this.mcs = new MinecraftServicesTokenManager(cache({ cacheName: 'mcs', username })) - this.pfb = new PlayfabTokenManager(cache({ cacheName: 'pfb', username })) + this.xbl = new XboxTokenManager(keyPair, cache({ cacheName: 'xbl', username }), this.fetch) + this.mba = new BedrockTokenManager(cache({ cacheName: 'bed', username }), this.fetch) + this.mca = new JavaTokenManager(cache({ cacheName: 'mca', username }), this.fetch) + this.mcs = new MinecraftServicesTokenManager(cache({ cacheName: 'mcs', username }), this.fetch) + this.pfb = new PlayfabTokenManager(cache({ cacheName: 'pfb', username }), this.fetch) } async getMsaToken () { diff --git a/src/TokenManagers/LiveTokenManager.js b/src/TokenManagers/LiveTokenManager.js index e1899d0..a035373 100644 --- a/src/TokenManagers/LiveTokenManager.js +++ b/src/TokenManagers/LiveTokenManager.js @@ -4,10 +4,11 @@ const { Endpoints } = require('../common/Constants') const { checkStatus } = require('../common/Util') class LiveTokenManager { - constructor (clientId, scopes, cache) { + constructor (clientId, scopes, cache, fetchFn = fetch) { this.clientId = clientId this.scopes = scopes this.cache = cache + this.fetch = fetchFn } async verifyTokens () { @@ -46,7 +47,7 @@ class LiveTokenManager { credentials: 'include' // This cookie handler does not work on node-fetch ... } - const token = await fetch(Endpoints.live.tokenRequest, codeRequest).then(checkStatus) + const token = await this.fetch(Endpoints.live.tokenRequest, codeRequest).then(checkStatus) this.updateCache(token) return token } @@ -91,7 +92,7 @@ class LiveTokenManager { const cookies = [] - const res = await fetch(Endpoints.live.deviceCodeRequest, codeRequest) + const res = await this.fetch(Endpoints.live.deviceCodeRequest, codeRequest) .then(res => { if (res.status !== 200) { res.text().then(console.warn) @@ -128,7 +129,7 @@ class LiveTokenManager { }).toString() } - const token = await fetch(Endpoints.live.tokenRequest + '?client_id=' + this.clientId, verifi) + const token = await this.fetch(Endpoints.live.tokenRequest + '?client_id=' + this.clientId, verifi) .then(res => res.json()).then(res => { if (res.error) { if (res.error === 'authorization_pending') { diff --git a/src/TokenManagers/MinecraftBedrockServicesManager.js b/src/TokenManagers/MinecraftBedrockServicesManager.js index 5381c9d..1377e90 100644 --- a/src/TokenManagers/MinecraftBedrockServicesManager.js +++ b/src/TokenManagers/MinecraftBedrockServicesManager.js @@ -4,8 +4,9 @@ const { Endpoints } = require('../common/Constants') const { checkStatus } = require('../common/Util') class MinecraftBedrockServicesTokenManager { - constructor (cache) { + constructor (cache, fetchFn = fetch) { this.cache = cache + this.fetch = fetchFn } async getCachedAccessToken () { @@ -25,7 +26,7 @@ class MinecraftBedrockServicesTokenManager { } async getAccessToken (sessionTicket, options = {}) { - const response = await fetch(Endpoints.minecraftBedrock.servicesSessionStart, { + const response = await this.fetch(Endpoints.minecraftBedrock.servicesSessionStart, { method: 'post', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/src/TokenManagers/MinecraftBedrockTokenManager.js b/src/TokenManagers/MinecraftBedrockTokenManager.js index 7a20a76..d41ddd2 100644 --- a/src/TokenManagers/MinecraftBedrockTokenManager.js +++ b/src/TokenManagers/MinecraftBedrockTokenManager.js @@ -4,8 +4,9 @@ const { Endpoints } = require('../common/Constants') const { checkStatusWithHelp } = require('../common/Util') class BedrockTokenManager { - constructor (cache) { + constructor (cache, fetchFn = fetch) { this.cache = cache + this.fetch = fetchFn } async getCachedAccessToken () { @@ -51,7 +52,7 @@ class BedrockTokenManager { 'User-Agent': 'MCPE/UWP', Authorization: `XBL3.0 x=${xsts.userHash};${xsts.XSTSToken}` } - const MineServicesResponse = await fetch(Endpoints.minecraftBedrock.authenticate, { + const MineServicesResponse = await this.fetch(Endpoints.minecraftBedrock.authenticate, { method: 'post', headers, body: JSON.stringify({ identityPublicKey: clientPublicKey }) diff --git a/src/TokenManagers/MinecraftJavaTokenManager.js b/src/TokenManagers/MinecraftJavaTokenManager.js index c190833..743ac85 100644 --- a/src/TokenManagers/MinecraftJavaTokenManager.js +++ b/src/TokenManagers/MinecraftJavaTokenManager.js @@ -28,8 +28,9 @@ const reportReasons = { const toDER = pem => pem.split('\n').slice(1, -1).reduce((acc, cur) => Buffer.concat([acc, Buffer.from(cur, 'base64')]), Buffer.alloc(0)) class MinecraftJavaTokenManager { - constructor (cache) { + constructor (cache, fetchFn = fetch) { this.cache = cache + this.fetch = fetchFn } async getCachedAccessToken () { @@ -65,7 +66,7 @@ class MinecraftJavaTokenManager { async getAccessToken (xsts) { debug('[mc] authing to minecraft', xsts) - const MineServicesResponse = await fetch(Endpoints.minecraftJava.loginWithXbox, { + const MineServicesResponse = await this.fetch(Endpoints.minecraftJava.loginWithXbox, { method: 'post', ...fetchOptions, body: JSON.stringify({ identityToken: `XBL3.0 x=${xsts.userHash};${xsts.XSTSToken}` }) @@ -79,7 +80,7 @@ class MinecraftJavaTokenManager { async fetchProfile (accessToken) { debug(`[mc] fetching minecraft profile with ${accessToken.slice(0, 16)}`) const headers = { ...fetchOptions.headers, Authorization: `Bearer ${accessToken}` } - const profile = await fetch(Endpoints.minecraftJava.profile, { headers }) + const profile = await this.fetch(Endpoints.minecraftJava.profile, { headers }) .then(checkStatus) debug(`[mc] got profile response: ${profile}`) return profile @@ -93,7 +94,7 @@ class MinecraftJavaTokenManager { async fetchEntitlements (accessToken) { debug(`[mc] fetching entitlements with ${accessToken.slice(0, 16)}`) const headers = { ...fetchOptions.headers, Authorization: `Bearer ${accessToken}` } - const entitlements = await fetch(Endpoints.minecraftJava.entitlements + `?requestId=${crypto.randomUUID()}`, { headers }).then(checkStatus) + const entitlements = await this.fetch(Endpoints.minecraftJava.entitlements + `?requestId=${crypto.randomUUID()}`, { headers }).then(checkStatus) debug(`[mc] got entitlement response: ${entitlements}`) return entitlements } @@ -101,7 +102,7 @@ class MinecraftJavaTokenManager { async fetchCertificates (accessToken) { debug(`[mc] fetching key-pair with ${accessToken.slice(0, 16)}`) const headers = { ...fetchOptions.headers, Authorization: `Bearer ${accessToken}` } - const cert = await fetch(Endpoints.minecraftJava.certificates, { method: 'post', headers }).then(checkStatus) + const cert = await this.fetch(Endpoints.minecraftJava.certificates, { method: 'post', headers }).then(checkStatus) debug('[mc] got key-pair') const profileKeys = { publicPEM: cert.keyPair.publicKey, @@ -176,7 +177,7 @@ class MinecraftJavaTokenManager { } debug('[mc] reporting player with payload', body) - const reportResponse = await fetch(Endpoints.minecraftJava.reportPlayer, { method: 'post', headers, body }).then(checkStatus) + const reportResponse = await this.fetch(Endpoints.minecraftJava.reportPlayer, { method: 'post', headers, body }).then(checkStatus) debug('[mc] server response for report', reportResponse) return true } diff --git a/src/TokenManagers/PlayfabTokenManager.js b/src/TokenManagers/PlayfabTokenManager.js index 1b64e9d..9947a1f 100644 --- a/src/TokenManagers/PlayfabTokenManager.js +++ b/src/TokenManagers/PlayfabTokenManager.js @@ -2,8 +2,9 @@ const debug = require('debug')('prismarine-auth') const { Endpoints } = require('../common/Constants') class PlayfabTokenManager { - constructor (cache) { + constructor (cache, fetchFn = fetch) { this.cache = cache + this.fetch = fetchFn } async setCachedAccessToken (data) { @@ -21,7 +22,7 @@ class PlayfabTokenManager { } async getAccessToken (xsts) { - const response = await fetch(Endpoints.PlayfabLoginWithXbox, { + const response = await this.fetch(Endpoints.PlayfabLoginWithXbox, { method: 'post', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/src/TokenManagers/XboxTokenManager.js b/src/TokenManagers/XboxTokenManager.js index 684d392..46d8669 100644 --- a/src/TokenManagers/XboxTokenManager.js +++ b/src/TokenManagers/XboxTokenManager.js @@ -18,10 +18,11 @@ const checkIfValid = (expires) => { // Manages Xbox Live tokens for xboxlive.com class XboxTokenManager { - constructor (ecKey, cache) { + constructor (ecKey, cache, fetchFn = fetch) { this.key = ecKey this.jwk = { ...ecKey.publicKey.export({ format: 'jwk' }), alg: 'ES256', use: 'sig' } this.cache = cache + this.fetch = fetchFn this.headers = { 'Cache-Control': 'no-store, must-revalidate, no-cache', 'x-xbl-contract-version': 1 } } @@ -78,7 +79,7 @@ class XboxTokenManager { const signature = this.sign(Endpoints.xbox.userAuth, '', body).toString('base64') const headers = { ...this.headers, signature, 'Content-Type': 'application/json', accept: 'application/json', 'x-xbl-contract-version': '2' } - const ret = await fetch(Endpoints.xbox.userAuth, { method: 'post', headers, body }).then(checkStatus) + const ret = await this.fetch(Endpoints.xbox.userAuth, { method: 'post', headers, body }).then(checkStatus) await this.setCachedToken({ userToken: ret }) @@ -151,7 +152,7 @@ class XboxTokenManager { const headers = { Signature: signature } - const req = await fetch(Endpoints.xbox.sisuAuthorize, { method: 'post', headers, body }) + const req = await this.fetch(Endpoints.xbox.sisuAuthorize, { method: 'post', headers, body }) const ret = await req.json() if (!req.ok) this.checkTokenError(parseInt(req.headers.get('x-err')), ret) @@ -190,7 +191,7 @@ class XboxTokenManager { const headers = { ...this.headers, Signature: signature } - const req = await fetch(Endpoints.xbox.xstsAuthorize, { method: 'post', headers, body }) + const req = await this.fetch(Endpoints.xbox.xstsAuthorize, { method: 'post', headers, body }) const ret = await req.json() if (!req.ok) this.checkTokenError(ret.XErr, ret) @@ -229,7 +230,7 @@ class XboxTokenManager { const signature = this.sign(Endpoints.xbox.deviceAuth, '', body).toString('base64') const headers = { ...this.headers, Signature: signature } - const ret = await fetch(Endpoints.xbox.deviceAuth, { method: 'post', headers, body }).then(checkStatus) + const ret = await this.fetch(Endpoints.xbox.deviceAuth, { method: 'post', headers, body }).then(checkStatus) await this.setCachedToken({ deviceToken: ret }) @@ -255,7 +256,7 @@ class XboxTokenManager { const headers = { ...this.headers, Signature: signature } - const ret = await fetch(Endpoints.xbox.titleAuth, { method: 'post', headers, body }).then(checkStatus) + const ret = await this.fetch(Endpoints.xbox.titleAuth, { method: 'post', headers, body }).then(checkStatus) await this.setCachedToken({ titleToken: ret })