minecraft-protocol
Version:
Parse and serialize minecraft packets, plus authentication and encryption.
177 lines (164 loc) • 7.23 kB
JavaScript
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)
}
}