UNPKG

minecraft-protocol

Version:

Parse and serialize minecraft packets, plus authentication and encryption.

177 lines (164 loc) 7.23 kB
const UUID = require('uuid-1345') const yggdrasil = require('yggdrasil') const fs = require('fs').promises const mcDefaultFolderPath = require('minecraft-folder-path') const path = require('path') const launcherDataFile = 'launcher_accounts.json' module.exports = async function (client, options) { if (!options.profilesFolder && options.profilesFolder !== false) { // not defined, but not explicitly false. fallback to default let mcFolderExists = true try { await fs.access(mcDefaultFolderPath) } catch (ignoreErr) { mcFolderExists = false } options.profilesFolder = mcFolderExists ? mcDefaultFolderPath : '.' // local folder if mc folder doesn't exist } const yggdrasilClient = yggdrasil({ agent: options.agent, host: options.authServer || 'https://authserver.mojang.com' }) const clientToken = options.clientToken || (options.session && options.session.clientToken) || (options.profilesFolder && (await getLauncherProfiles()).mojangClientToken) || UUID.v4().toString().replace(/-/g, '') const skipValidation = false || options.skipValidation options.accessToken = null options.haveCredentials = !!options.password || (clientToken != null && options.session != null) || (options.profilesFolder && !!getProfileId(await getLauncherProfiles())) async function getLauncherProfiles () { // get launcher profiles try { return JSON.parse(await fs.readFile(path.join(options.profilesFolder, launcherDataFile), 'utf8')) } catch (err) { await fs.mkdir(options.profilesFolder, { recursive: true }) await fs.writeFile(path.join(options.profilesFolder, launcherDataFile), '{}') return { accounts: {} } } } function getProfileId (auths) { try { const lowerUsername = options.username.toLowerCase() return Object.keys(auths.accounts).find(key => auths.accounts[key].username.toLowerCase() === lowerUsername || auths.accounts[key].minecraftProfile.name.toLowerCase() === lowerUsername ) } catch (err) { return false } } if (options.haveCredentials) { // make a request to get the case-correct username before connecting. const cb = function (err, session) { if (options.profilesFolder) { getLauncherProfiles().then((auths) => { if (!auths.accounts) auths.accounts = [] try { let profile = getProfileId(auths) if (err) { if (profile && auths.accounts[profile].type !== 'Xbox') { // MS accounts are deemed invalid in case someone tries to use one without specifying options.auth, but we shouldn't remove these delete auths.accounts[profile] // profile is invalid, remove } } else { // successful login if (!profile) { profile = UUID.v4().toString().replace(/-/g, '') // create new profile throw new Error('Account not found') // TODO: Find a way to calculate remoteId. Launcher ignores account entry and makes a new one if remoteId is incorrect } if (!auths.accounts[profile].remoteId) { delete auths.accounts[profile] throw new Error('Account has no remoteId') // TODO: Find a way to calculate remoteId. Launcher ignores account entry and makes a new one if remoteId is incorrect } if (!auths.mojangClientToken) { auths.mojangClientToken = clientToken } if (clientToken === auths.mojangClientToken) { // only do something when we can save a new clienttoken or they match const oldProfileObj = auths.accounts[profile] const newProfileObj = { accessToken: session.accessToken, minecraftProfile: { id: session.selectedProfile.id, name: session.selectedProfile.name }, userProperites: oldProfileObj?.userProperites ?? [], remoteId: oldProfileObj?.remoteId ?? '', username: options.username, localId: profile, type: (options.auth?.toLowerCase() === 'mojang' ? 'Mojang' : 'Xbox'), persistent: true } auths.accounts[profile] = newProfileObj } } } catch (ignoreErr) { // again, silently fail, just don't save anything } fs.writeFile(path.join(options.profilesFolder, launcherDataFile), JSON.stringify(auths, null, 2)).then(() => {}, (ignoreErr) => { // console.warn("Couldn't save tokens:\n", err) // not any error, we just don't save the file }) }, (ignoreErr) => { // console.warn("Skipped saving tokens because of error\n", err) // not any error, we just don't save the file }) } if (err) { client.emit('error', err) } else { client.session = session client.username = session.selectedProfile.name options.accessToken = session.accessToken client.emit('session', session) options.connect(client) } } if (!options.session && options.profilesFolder) { try { const auths = await getLauncherProfiles() const profile = getProfileId(auths) if (profile) { const newUsername = auths.accounts[profile].username const displayName = auths.accounts[profile].minecraftProfile.name const uuid = auths.accounts[profile].minecraftProfile.id const newProfile = { id: uuid, name: displayName } options.session = { accessToken: auths.accounts[profile].accessToken, clientToken: auths.mojangClientToken, selectedProfile: newProfile, availableProfiles: [newProfile] } options.username = newUsername } } catch (ignoreErr) { // skip the error :/ } } if (options.session) { if (!skipValidation) { yggdrasilClient.validate(options.session.accessToken, function (err) { if (!err) { cb(null, options.session) } else { yggdrasilClient.refresh(options.session.accessToken, options.session.clientToken, function (err, accessToken, data) { if (!err) { cb(null, data) } else if (options.username && options.password) { yggdrasilClient.auth({ user: options.username, pass: options.password, token: clientToken, requestUser: true }, cb) } else { cb(err, data) } }) } }) } else { // trust that the provided session is a working one cb(null, options.session) } } else { yggdrasilClient.auth({ user: options.username, pass: options.password, token: clientToken }, cb) } } else { // assume the server is in offline mode and just go for it. client.username = options.username options.connect(client) } }