Realm Joining (#1056)

* Add docs

* Implement Realm joining

* Check for Microsoft auth

* Remove on chat event

* Add realmName option to example

* Passthrough client to `realmAuthenticate`

* Fix overwriting existing authflow

* Don't use  `??=`

* Lint
This commit is contained in:
LucienHH 2023-01-15 17:30:33 +00:00 committed by GitHub
parent 0526edf5f3
commit 28093fb1fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 107 additions and 7 deletions

View file

@ -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])

View file

@ -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

View file

@ -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)
})

View file

@ -0,0 +1,8 @@
{
"name": "node-minecraft-protocol-example",
"version": "0.0.0",
"private": true,
"dependencies": {
},
"description": "A node-minecraft-protocol example"
}

View file

@ -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",

View file

@ -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
}

View file

@ -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:

7
src/index.d.ts vendored
View file

@ -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