UNPKG

pocket-minecraft-protocol

Version:

Parse and serialize Minecraft Bedrock Edition packets

306 lines (273 loc) 8.92 kB
const msal = require('@azure/msal-node') const XboxLiveAuth = require('@xboxreplay/xboxlive-auth') const debug = require('debug')('minecraft-protocol') const fs = require('fs') const path = require('path') const fetch = require('node-fetch') const authConstants = require('./authConstants') // Manages Microsoft account tokens class MsaTokenManager { constructor (msalConfig, scopes, cacheLocation) { this.msaClientId = msalConfig.auth.clientId this.scopes = scopes this.cacheLocation = cacheLocation || path.join(__dirname, './msa-cache.json') this.reloadCache() const beforeCacheAccess = async (cacheContext) => { cacheContext.tokenCache.deserialize(await fs.promises.readFile(this.cacheLocation, 'utf-8')) } const afterCacheAccess = async (cacheContext) => { if (cacheContext.cacheHasChanged) { await fs.promises.writeFile(this.cacheLocation, cacheContext.tokenCache.serialize()) } } const cachePlugin = { beforeCacheAccess, afterCacheAccess } msalConfig.cache = { cachePlugin } this.msalApp = new msal.PublicClientApplication(msalConfig) this.msalConfig = msalConfig } reloadCache () { try { this.msaCache = require(this.cacheLocation) } catch (e) { this.msaCache = {} fs.writeFileSync(this.cacheLocation, JSON.stringify(this.msaCache)) } } getUsers () { const accounts = this.msaCache.Account const users = [] if (!accounts) return users for (const account of Object.values(accounts)) { users.push(account) } return users } getAccessToken () { const tokens = this.msaCache.AccessToken if (!tokens) return const account = Object.values(tokens).filter(t => t.client_id === this.msaClientId)[0] if (!account) { debug('[msa] No valid access token found', tokens) return } const until = new Date(account.expires_on * 1000) - Date.now() const valid = until > 1000 return { valid, until: until, token: account.secret } } getRefreshToken () { const tokens = this.msaCache.RefreshToken if (!tokens) return const account = Object.values(tokens).filter(t => t.client_id === this.msaClientId)[0] if (!account) { debug('[msa] No valid refresh token found', tokens) return } return { token: account.secret } } async refreshTokens () { const rtoken = this.getRefreshToken() if (!rtoken) { throw new Error('Cannot refresh without refresh token') } const refreshTokenRequest = { refreshToken: rtoken.token, scopes: this.scopes } return new Promise((resolve, reject) => { this.msalApp.acquireTokenByRefreshToken(refreshTokenRequest).then((response) => { debug('[msa] refreshed token', JSON.stringify(response)) this.reloadCache() resolve(response) }).catch((error) => { debug('[msa] failed to refresh', JSON.stringify(error)) reject(error) }) }) } async verifyTokens () { const at = this.getAccessToken() const rt = this.getRefreshToken() if (!at || !rt || this.forceRefresh) { return false } debug('[msa] have at, rt', at, rt) if (at.valid && rt) { return true } else { try { await this.refreshTokens() return true } catch (e) { console.warn('Error refreshing token', e) // TODO: looks like an error happens here return false } } } // Authenticate with device_code flow async authDeviceCode (dataCallback) { const deviceCodeRequest = { deviceCodeCallback: (resp) => { debug('[msa] device_code response: ', resp) dataCallback(resp) }, scopes: this.scopes } return new Promise((resolve, reject) => { this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest).then((response) => { debug('[msa] device_code resp', JSON.stringify(response)) if (!this.msaCache.Account) this.msaCache.Account = { '': response.account } resolve(response) }).catch((error) => { console.warn('[msa] Error getting device code') console.debug(JSON.stringify(error)) reject(error) }) }) } } // Manages Xbox Live tokens for xboxlive.com class XboxTokenManager { constructor (relyingParty, cacheLocation) { this.relyingParty = relyingParty this.cacheLocation = cacheLocation || path.join(__dirname, './xbl-cache.json') try { this.cache = require(this.cacheLocation) } catch (e) { this.cache = {} } } getCachedUserToken () { const token = this.cache.userToken if (!token) return const until = new Date(token.NotAfter) const dn = Date.now() const remainingMs = until - dn const valid = remainingMs > 1000 return { valid, token: token.Token, data: token } } getCachedXstsToken () { const token = this.cache.xstsToken if (!token) return const until = new Date(token.expiresOn) const dn = Date.now() const remainingMs = until - dn const valid = remainingMs > 1000 return { valid, token: token.XSTSToken, data: token } } setCachedUserToken (data) { this.cache.userToken = data fs.writeFileSync(this.cacheLocation, JSON.stringify(this.cache)) } setCachedXstsToken (data) { this.cache.xstsToken = data fs.writeFileSync(this.cacheLocation, JSON.stringify(this.cache)) } async verifyTokens () { const ut = this.getCachedUserToken() const xt = this.getCachedXstsToken() if (!ut || !xt || this.forceRefresh) { return false } debug('[xbl] have user, xsts', ut, xt) if (ut.valid && xt.valid) { return true } else if (ut.valid && !xt.valid) { try { await this.getXSTSToken(ut.data) return true } catch (e) { return false } } return false } async getUserToken (msaAccessToken) { debug('[xbl] obtaining xbox token with ms token', msaAccessToken) if (!msaAccessToken.startsWith('d=')) { msaAccessToken = 'd=' + msaAccessToken } const xblUserToken = await XboxLiveAuth.exchangeRpsTicketForUserToken(msaAccessToken) this.setCachedUserToken(xblUserToken) debug('[xbl] user token:', xblUserToken) return xblUserToken } async getXSTSToken (xblUserToken) { debug('[xbl] obtaining xsts token with xbox user token', xblUserToken.Token) const xsts = await XboxLiveAuth.exchangeUserTokenForXSTSIdentity( xblUserToken.Token, { XSTSRelyingParty: this.relyingParty, raw: false } ) this.setCachedXstsToken(xsts) debug('[xbl] xsts', xsts) return xsts } } // Manages Minecraft tokens for sessionserver.mojang.com class MinecraftTokenManager { constructor (clientPublicKey, cacheLocation) { this.clientPublicKey = clientPublicKey this.cacheLocation = cacheLocation || path.join(__dirname, './bed-cache.json') try { this.cache = require(this.cacheLocation) } catch (e) { this.cache = {} } } getCachedAccessToken () { const token = this.cache.mca debug('[mc] token cache', this.cache) if (!token) return console.log('TOKEN', token) const jwt = token.chain[0] const [header, payload, signature] = jwt.split('.').map(k => Buffer.from(k, 'base64')) // eslint-disable-line const body = JSON.parse(String(payload)) const expires = new Date(body.exp * 1000) const remainingMs = expires - Date.now() const valid = remainingMs > 1000 return { valid, until: expires, chain: token.chain } } setCachedAccessToken (data) { data.obtainedOn = Date.now() this.cache.mca = data fs.writeFileSync(this.cacheLocation, JSON.stringify(this.cache)) } async verifyTokens () { const at = this.getCachedAccessToken() if (!at || this.forceRefresh) { return false } debug('[mc] have user access token', at) if (at.valid) { return true } return false } async getAccessToken (clientPublicKey, xsts) { debug('[mc] authing to minecraft', clientPublicKey, xsts) const getFetchOptions = { headers: { 'Content-Type': 'application/json', 'User-Agent': 'node-minecraft-protocol', Authorization: `XBL3.0 x=${xsts.userHash};${xsts.XSTSToken}` } } const MineServicesResponse = await fetch(authConstants.MinecraftAuth, { method: 'post', ...getFetchOptions, body: JSON.stringify({ identityPublicKey: clientPublicKey }) }).then(checkStatus) debug('[mc] mc auth response', MineServicesResponse) this.setCachedAccessToken(MineServicesResponse) return MineServicesResponse } } function checkStatus (res) { if (res.ok) { // res.status >= 200 && res.status < 300 return res.json() } else { throw Error(res.statusText) } } module.exports = { MsaTokenManager, XboxTokenManager, MinecraftTokenManager }