diff --git a/docs/API.md b/docs/API.md index 5719d55..ac5c0e8 100644 --- a/docs/API.md +++ b/docs/API.md @@ -139,6 +139,10 @@ Returns a `Client` instance and perform login. * id : a numeric client id used for referring to multiple clients in a server * validateChannelProtocol (optional) : whether or not to enable protocol validation for custom protocols using plugin channels. Defaults to true * disableChatSigning (optional) : Don't try obtaining chat signing keys from Mojang (1.19+) + * realms : An object which should contain one of the following properties: `realmId` or `pickRealm`. When defined will attempt to join a Realm without needing to specify host/port. **The authenticated account must either own the Realm or have been invited to it** + * realmId : The id of the Realm to join. + * pickRealm(realms) : A function which will have an array of the user Realms (joined/owned) passed to it. The function should return a Realm. + ## mc.Client(isServer,version,[customPackets]) diff --git a/docs/README.md b/docs/README.md index 2017ad3..25a542e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -95,6 +95,20 @@ client.on('chat', function(packet) { If the server is in offline mode, you may leave out the `password` option and switch auth to `offline`. You can also leave out `password` when using a Microsoft account. If provided, password based auth will be attempted first which may fail. *Note:* if using a Microsoft account, your account age must be >= 18 years old. +### Client example joining a Realm + +Example to connect to a Realm that the authenticating account is owner of or has been invited to: + +```js +var mc = require('minecraft-protocol'); +var client = mc.createClient({ + realms: { + pickRealm: (realms) => realms[0] // Function which recieves an array of joined/owned Realms and must return a single Realm. Can be async + }, + auth: 'microsoft' +}) +``` + ### Hello World server example ```js diff --git a/examples/client_realms/client_realms.js b/examples/client_realms/client_realms.js new file mode 100644 index 0000000..14985f8 --- /dev/null +++ b/examples/client_realms/client_realms.js @@ -0,0 +1,25 @@ +'use strict' + +const mc = require('minecraft-protocol') + +const [,, username, realmName] = process.argv +if (!realmName) { + console.log('Usage : node client_realms.js <username/email> <realm_name>') + process.exit(1) +} + +const client = mc.createClient({ + realms: { + // realmId: '1234567', // Connect the client to a Realm using the Realms ID + pickRealm: (realms) => realms.find(e => e.name === realmName) // Connect the client to a Realm using a function that returns a Realm + }, + username, + auth: 'microsoft' // This option must be present and set to 'microsoft' to join a Realm. +}) + +client.on('connect', function () { + console.info('connected') +}) +client.on('disconnect', function (packet) { + console.log('disconnected: ' + packet.reason) +}) diff --git a/examples/client_realms/package.json b/examples/client_realms/package.json new file mode 100644 index 0000000..56fcdf2 --- /dev/null +++ b/examples/client_realms/package.json @@ -0,0 +1,8 @@ +{ + "name": "node-minecraft-protocol-example", + "version": "0.0.0", + "private": true, + "dependencies": { + }, + "description": "A node-minecraft-protocol example" +} diff --git a/package.json b/package.json index 1849a7b..9f7f6c2 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "node-rsa": "^0.4.2", "prismarine-auth": "^2.2.0", "prismarine-nbt": "^2.0.0", + "prismarine-realms": "^1.2.0", "protodef": "^1.8.0", "readable-stream": "^4.1.0", "uuid-1345": "^1.0.1", diff --git a/src/client/microsoftAuth.js b/src/client/microsoftAuth.js index e438fb2..8aea989 100644 --- a/src/client/microsoftAuth.js +++ b/src/client/microsoftAuth.js @@ -2,19 +2,23 @@ const path = require('path') const { Authflow: PrismarineAuth, Titles } = require('prismarine-auth') const minecraftFolderPath = require('minecraft-folder-path') const debug = require('debug')('minecraft-protocol') +const { RealmAPI } = require('prismarine-realms') -async function authenticate (client, options) { +function validateOptions (options) { if (!options.profilesFolder) { options.profilesFolder = path.join(minecraftFolderPath, 'nmp-cache') } - if (options.authTitle === undefined) { options.authTitle = Titles.MinecraftNintendoSwitch options.deviceType = 'Nintendo' options.flow = 'live' } +} - client.authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode) +async function authenticate (client, options) { + validateOptions(options) + + if (!client.authflow) client.authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode) const { token, entitlements, profile, certificates } = await client.authflow.getMinecraftJavaToken({ fetchProfile: true, fetchCertificates: !options.disableChatSigning }).catch(e => { if (options.password) console.warn('Sign in failed, try removing the password field\n') if (e.toString().includes('Not Found')) console.warn(`Please verify that the account ${options.username} owns Minecraft\n`) @@ -42,6 +46,38 @@ async function authenticate (client, options) { options.connect(client) } -module.exports = { - authenticate +async function realmAuthenticate (client, options) { + validateOptions(options) + + client.authflow = new PrismarineAuth(options.username, options.profilesFolder, options, options.onMsaCode) + + const api = RealmAPI.from(client.authflow, 'java') + const realms = await api.getRealms() + + debug('realms', realms) + + if (!realms || !realms.length) throw Error('Couldn\'t find any Realms for the authenticated account') + + let realm + + if (options.realms.realmId) { + realm = realms.find(e => e.id === Number(options.realms.realmId)) + } else if (options.realms.pickRealm) { + if (typeof options.realms.pickRealm !== 'function') throw Error('realms.pickRealm must be a function') + realm = await options.realms.pickRealm(realms) + } + + if (!realm) throw Error('Couldn\'t find a Realm to connect to. Authenticated account must be the owner or has been invited to the Realm.') + + const { host, port } = await realm.getAddress() + + debug('realms connection', { host, port }) + + options.host = host + options.port = port +} + +module.exports = { + authenticate, + realmAuthenticate } diff --git a/src/createClient.js b/src/createClient.js index aeea6ac..1c4f033 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -20,7 +20,8 @@ module.exports = createClient function createClient (options) { assert.ok(options, 'options is required') assert.ok(options.username, 'username is required') - if (!options.version) { options.version = false } + if (!options.version && !options.realms) { options.version = false } + if (options.realms && options.auth !== 'microsoft') throw new Error('Currently Realms can only be joined with auth: "microsoft"') // TODO: avoid setting default version if autoVersion is enabled const optVersion = options.version || require('./version').defaultVersion @@ -43,7 +44,11 @@ function createClient (options) { auth(client, options) break case 'microsoft': - microsoftAuth.authenticate(client, options).catch((err) => client.emit('error', err)) + if (options.realms) { + microsoftAuth.realmAuthenticate(client, options).then(() => microsoftAuth.authenticate(client, options)).catch((err) => client.emit('error', err)) + } else { + microsoftAuth.authenticate(client, options).catch((err) => client.emit('error', err)) + } break case 'offline': default: diff --git a/src/index.d.ts b/src/index.d.ts index 24aa15b..0bfef3d 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -6,6 +6,7 @@ import * as Stream from 'stream' import { Agent } from 'http' import { Transform } from "readable-stream"; import { KeyObject } from 'crypto'; +import { Realm } from "prismarine-realms" type PromiseLike = Promise<void> | void @@ -131,6 +132,7 @@ declare module 'minecraft-protocol' { id?: number session?: SessionOption validateChannelProtocol?: boolean, + realms?: RealmsOptions // 1.19+ disableChatSigning?: boolean } @@ -259,6 +261,11 @@ declare module 'minecraft-protocol' { latency: number } + export interface RealmsOptions { + realmId?: string + pickRealm?: (realms: Realm[]) => Realm + } + export const states: typeof States export const supportedVersions: string[] export const defaultVersion: string