Enforce server auth, ping on createClient, update docs (#68)
* Add createServer, ping on createClient, update README * fix createClient keepalive * resort readme, fix node 14 * Enforce auth on server connections, fix close/connect issues * add type definitions, update readme, docs * Wait some time before closing connection, update docs * wait for server close in tests, fix race bug * export a ping api * Rename api.md to API.md * add ping example
This commit is contained in:
parent
d3723ef42a
commit
999bfd3569
29 changed files with 580 additions and 135 deletions
|
|
@ -1 +1,7 @@
|
|||
node_modules
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
__*
|
||||
src/**/*.json
|
||||
# Runtime generated data
|
||||
data/**/sample
|
||||
tools/bds*
|
||||
123
README.md
123
README.md
|
|
@ -1,33 +1,122 @@
|
|||
# bedrock-protocol
|
||||
# Minecraft Bedrock protocol library
|
||||
[](http://npmjs.com/package/bedrock-protocol)
|
||||
[](https://github.com/PrismarineJS/bedrock-protocol/actions?query=workflow%3A%22CI%22)
|
||||
[](https://discord.gg/GsEFRM8)
|
||||
[](https://gitpod.io/#https://github.com/PrismarineJS/bedrock-protocol)
|
||||
|
||||
Not ready for prime time yet, check https://github.com/PrismarineJS/bedrock-protocol/projects/1 for progress
|
||||
|
||||
Parse and serialize Minecraft: Pocket Edition packets
|
||||
Minecraft Bedrock Edition (aka MCPE) protocol library, supporting authentication and encryption. Help [contribute](CONTRIBUTING.md).
|
||||
|
||||
This is a work in progress. You can track the progress in https://github.com/PrismarineJS/bedrock-protocol/pull/34.
|
||||
|
||||
## Features
|
||||
|
||||
* Supports Minecraft Pocket Edition `1.0`
|
||||
* Pure JavaScript
|
||||
* Easily send and listen for any packet
|
||||
* RakNet support through [node-raknet](https://github.com/mhsjlw/node-raknet)
|
||||
- Supports Minecraft Bedrock version 1.16.201, 1.16.210, 1.16.220
|
||||
- Parses and serialize all packets with JavaScript objects
|
||||
- Send a packet by supplying fields as a JavaScript object
|
||||
- Automatically respond to keep-alive packets
|
||||
- Client
|
||||
- Authentication and login
|
||||
- Encryption
|
||||
- Online mode servers
|
||||
- [Ping a server for status](docs/API.md#beping-host-port---serveradvertisement)
|
||||
- Server
|
||||
- Autheticate clients with Xbox Live
|
||||
- Ping status
|
||||
- Automatically respond to keep-alive packets
|
||||
|
||||
* Robust test coverage.
|
||||
* Easily extend with many other PrismarineJS projects, world providers, and more
|
||||
* Optimized for rapidly staying up to date with Minecraft protocol updates.
|
||||
|
||||
|
||||
Want to contribute on something important for PrismarineJS ? go to https://github.com/PrismarineJS/mineflayer/wiki/Big-Prismarine-projects
|
||||
|
||||
## Installation
|
||||
Simply run
|
||||
|
||||
npm install bedrock-protocol
|
||||
`npm install bedrock-protocol`
|
||||
|
||||
Then view our `examples` for inspiration!
|
||||
## Usage
|
||||
|
||||
## Contributors
|
||||
This project is run by these guys:
|
||||
### Client example
|
||||
|
||||
- [mhsjlw](https://github.com/mhsjlw)
|
||||
- [rom1504](https://github.com/rom1504)
|
||||
- [Filiph Sandström](https://github.com/filfat)
|
||||
```js
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const client = bedrock.createClient({
|
||||
host: 'localhost', // optional
|
||||
port: 19132, // optional, default 19132
|
||||
username: 'Notch', // the username you want to join as, optional if online mode
|
||||
offline: true // optional, default false. if true, do not login with Xbox Live. You will not be asked to sign-in if set to true.
|
||||
})
|
||||
|
||||
## License
|
||||
Licensed under the MIT license.
|
||||
client.on('text', (packet) => { // Listen for chat messages and echo them back.
|
||||
if (packet.source_name != client.options.username) {
|
||||
client.queue('text', {
|
||||
type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '',
|
||||
message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}`
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Server example
|
||||
|
||||
*Can't connect locally on Windows? See the [faq](docs/FAQ.md)*
|
||||
```js
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const server = new bedrock.createServer({
|
||||
host: '0.0.0.0', // optional. Hostname to bind as.
|
||||
port: 19132, // optional
|
||||
version: '1.16.220' // optional. The server version, latest if not specified.
|
||||
})
|
||||
|
||||
server.on('connect', client => {
|
||||
client.on('join', () => { // The client has joined the server.
|
||||
const d = new Date() // Once client is in the server, send a colorful kick message
|
||||
client.disconnect(`Good ${d.getHours() < 12 ? '§emorning§r' : '§3afternoon§r'} :)\n\nMy time is ${d.toLocaleString()} !`)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Ping example
|
||||
|
||||
```js
|
||||
const { ping } = require('bedrock-protocol')
|
||||
ping({ host: 'play.cubecraft.net', port: 19132 }).then(res => {
|
||||
console.log(res)
|
||||
})
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
See [API documentation](docs/API.md)
|
||||
|
||||
See [faq](docs/FAQ.md)
|
||||
|
||||
<!-- ## Projects Using bedrock-protocol
|
||||
|
||||
* [mineflayer](https://github.com/PrismarineJS/mineflayer/) - create bots with a stable, high level API.
|
||||
* [pakkit](https://github.com/Heath123/pakkit) To monitor your packets
|
||||
* [flying-squid](https://github.com/PrismarineJS/flying-squid/) - create minecraft bots with a stable, high level API. -->
|
||||
|
||||
## Testing
|
||||
|
||||
```npm test```
|
||||
|
||||
## Debugging
|
||||
|
||||
You can enable some protocol debugging output using `DEBUG` environment variable.
|
||||
|
||||
Through node.js, add `process.env.DEBUG = 'minecraft-protocol'` at the top of your script.
|
||||
|
||||
## Contribute
|
||||
|
||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md) and https://github.com/PrismarineJS/prismarine-contribute
|
||||
|
||||
## History
|
||||
|
||||
See [history](HISTORY.md)
|
||||
|
||||
<!-- ## Related
|
||||
|
||||
* [map-colors](https://github.com/AresRPG/aresrpg-map-colors) can be used to convert any image into a buffer of minecraft compatible colors -->
|
||||
115
docs/API.md
Normal file
115
docs/API.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Documentation
|
||||
|
||||
## be.createClient(options) : Client
|
||||
|
||||
Returns a `Client` instance and connects to the server.
|
||||
|
||||
`options` is an object containing the properties :
|
||||
|
||||
| Paramater | Optionality | Description |
|
||||
| ----------- | ----------- |-|
|
||||
| host | **Required** | Hostname to connect to, for example `127.0.0.1`. |
|
||||
| port | *optional* | port to connect to, default to **19132** |
|
||||
| version | *optional* | Version to connect as. <br/>(Future feature, see [#69][1]) If not specified, should automatically match server version. <br/>(Current feature) Defaults to latest version. |
|
||||
| offline | *optional* | default to **false**. Set this to true to disable Microsoft/Xbox auth. |
|
||||
| username | Conditional | Required if `offline` set to true : Username to connect to server as. |
|
||||
| connectTimeout | *optional* | default to **9000ms**. How long to wait in milliseconds while trying to connect to server. |
|
||||
| onMsaCode | *optional* | Callback called when signing in with a microsoft account with device code auth, `data` is an object documented [here](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code#device-authorization-response) |
|
||||
| autoInitPlayer | optional | default to true, If we should send SetPlayerInitialized to the server after getting play_status spawn. |
|
||||
|
||||
|
||||
## be.createServer(options) : Server
|
||||
|
||||
Returns a `Server` instance and starts listening for clients. All clients will be
|
||||
authenticated unless offline is set to true.
|
||||
|
||||
`options` is an object containing the properties :
|
||||
|
||||
| Paramater | Optionality | Description |
|
||||
| ----------- | ----------- |-|
|
||||
| host | **Required** | The hostname to bind to. use `0.0.0.0` to bind all IPv4 addresses. |
|
||||
| port | *optional* | the port to bind to, default **19132** |
|
||||
| version | *optional* | Version to run server as. Clients below this version will be kicked, clients above will still be permitted. |
|
||||
| offline | *optional* | default to **false**. Set this to true to disable Microsoft/Xbox auth enforcement. |
|
||||
| maxPlayers | *[Future][1]* | default to **3**. Set this to change the maximum number of players connected. |
|
||||
| kickTimeout | *[Future][1]* | How long to wait before kicking a unresponsive client. |
|
||||
| motd | *[Future][1]* | ServerAdvertisement instance. The server advertisment shown to clients, including the message of the day, level name. |
|
||||
| advertismentFn | *[Future][1]* | optional. Custom function to call that should return a ServerAdvertisement, used for setting the RakNet server PONG data. Overrides `motd`. |
|
||||
|
||||
## be.ping({ host, port }) : ServerAdvertisement
|
||||
|
||||
Ping a server and get the response. See type definitions for the structure.
|
||||
|
||||
## Methods
|
||||
|
||||
[See the type defintions for this library for more information on methods.](../index.d.ts)
|
||||
|
||||
Both Client and Server classes have `write(name, params)` and `queue(name, params)` methods. The former sends a packet immediately, and the latter queues them to be sent in the next packet batch. Prefer the latter for better performance and less blocking.
|
||||
|
||||
You can use `.close()` to terminate a connection, and `.disconnect(reason)` to gracefully kick a connected client.
|
||||
|
||||
## Server usage
|
||||
|
||||
You can create a server as such:
|
||||
```js
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const server = bedrock.createServer({
|
||||
host: '0.0.0.0', // the hostname to bind to, use '0.0.0.0' to bind all hostnames
|
||||
port: 19132, // optional, port to bind to, default 19132
|
||||
offline: false // default false. verify connections with XBL
|
||||
})
|
||||
```
|
||||
|
||||
Then you can listen for clients and their events:
|
||||
```js
|
||||
// The 'connect' event is emitted after a new client has started a connection with the server and is handshaking.
|
||||
// Its one paramater is the client class instance which handles this session from here on out.
|
||||
server.on('connect', (client) => {
|
||||
// 'join' is emitted after the client has authenticated & connection is now encrypted.
|
||||
client.on('join', () => {
|
||||
// Then we can continue with the server spawning sequence. See examples/serverTest.js for an example spawn sequence.
|
||||
})
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
Order of server client event emisions:
|
||||
* 'connect' - emitted by `Server` after a client first joins the server. Second paramater is a `ServerPlayer` instance.
|
||||
* 'login' - emitted by client after the client has been authenticated by the server
|
||||
* 'join' - the client is ready to recieve game packets after successful server-client handshake/encryption
|
||||
* 'spawn' - emitted after the client lets the server know that it has successfully spawned
|
||||
|
||||
## Client docs
|
||||
|
||||
You can create a server as such:
|
||||
```js
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const client = bedrock.createClient({
|
||||
host: '127.0.0.1', // the hostname to bind to, use '0.0.0.0' to bind all hostnames
|
||||
port: 19132, // optional, port to bind to, default 19132
|
||||
username: 'Notch' // Any profile name, only used internally for account caching. You'll
|
||||
// be asked to sign-in with Xbox Live the first time.
|
||||
})
|
||||
```
|
||||
|
||||
```js
|
||||
// The 'join' event is emitted after the player has authenticated
|
||||
// and is ready to recieve chunks and start game packets
|
||||
client.on('join', client => console.log('Player has joined!'))
|
||||
|
||||
// The 'spawn' event is emitted. The chunks have been sent and all is well.
|
||||
client.on('join', client => console.log('Player has spawned!'))
|
||||
|
||||
// We can listen for text packets. See proto.yml for documentation.
|
||||
client.on('text', (packet) => {
|
||||
console.log('Client got text packet', packet)
|
||||
})
|
||||
```
|
||||
|
||||
Order of client event emisions:
|
||||
* 'connect' - emitted after a client first joins the server
|
||||
* 'login' - emitted after the client has been authenticated by the server
|
||||
* 'join' - the client is ready to recieve game packets after successful server-client handshake
|
||||
* 'spawn' - emitted after the client has permission from the server to spawn
|
||||
|
||||
[1]: https://github.com/PrismarineJS/bedrock-protocol/issues/69
|
||||
7
docs/FAQ.md
Normal file
7
docs/FAQ.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
## Can’t connect to localhost Win10 server with Minecraft Win10 Edition
|
||||
|
||||
This issue occurs due to loopback restrictions on Windows 10 UWP apps. To lift this restriction, launch Windows PowerShell as an administrator and run the following:
|
||||
|
||||
```ps
|
||||
CheckNetIsolation LoopbackExempt -a -n="Microsoft.MinecraftUWP_8wekyb3d8bbwe"
|
||||
```
|
||||
16
docs/api.md
16
docs/api.md
|
|
@ -1,16 +0,0 @@
|
|||
# Documentation
|
||||
|
||||
## be.createClient(options)
|
||||
|
||||
Returns a `Client` instance and starts listening. All clients will be
|
||||
automatically logged in and validated against microsoft's auth.
|
||||
|
||||
`options` is an object containing the properties :
|
||||
* host : default to undefined which means listen to all available ipv4 and ipv6 adresses
|
||||
* port (optional) : default to 25565
|
||||
(see https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback for details)
|
||||
* kickTimeout (optional) : default to `10*1000` (10s), kick client that doesn't answer to keepalive after that time
|
||||
* version (optional) : default to latest stable version, version of server
|
||||
* autoInitPlayer (optional) : default to true, If we should send SetPlayerInitialized to the server after getting play_status spawn.
|
||||
* offline (optional) : default to false, whether to auth with microsoft
|
||||
* connectTimeout (optional) : default to 9000, ms to wait before aborting connection attempt
|
||||
17
examples/clientReadmeExample.js
Normal file
17
examples/clientReadmeExample.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* eslint-disable */
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const client = bedrock.createClient({
|
||||
host: 'localhost', // optional
|
||||
port: 19132, // optional, default 19132
|
||||
username: 'Notch', // the username you want to join as, optional if online mode
|
||||
offline: false // optional, default false. if true, do not login with Xbox Live. You will not be asked to sign-in if set to true.
|
||||
})
|
||||
|
||||
client.on('text', (packet) => { // Listen for chat messages and echo them back.
|
||||
if (packet.source_name != client.options.username) {
|
||||
client.queue('text', {
|
||||
type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '',
|
||||
message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}`
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -9,6 +9,7 @@ async function test () {
|
|||
// You can specify version by adding :
|
||||
// version: '1.16.210'
|
||||
})
|
||||
client.connect()
|
||||
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
|
|
|
|||
5
examples/ping.js
Normal file
5
examples/ping.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const { ping } = require('bedrock-protocol')
|
||||
|
||||
ping({ host: 'play.cubecraft.net', port: 19132 }).then(res => {
|
||||
console.log(res)
|
||||
})
|
||||
14
examples/serverReadmeExample.js
Normal file
14
examples/serverReadmeExample.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* eslint-disable */
|
||||
const bedrock = require('bedrock-protocol')
|
||||
const server = new bedrock.createServer({
|
||||
host: '0.0.0.0', // optional
|
||||
port: 19132, // optional
|
||||
version: '1.16.220' // The server version
|
||||
})
|
||||
|
||||
server.on('connect', client => {
|
||||
client.on('join', () => { // The client has joined the server.
|
||||
const d = new Date() // Once client is in the server, send a colorful kick message
|
||||
client.disconnect(`Good ${d.getHours() < 12 ? '§emorning§r' : '§3afternoon§r'} :)\n\nMy time is ${d.toLocaleString()} !`)
|
||||
})
|
||||
})
|
||||
|
|
@ -71,25 +71,25 @@ async function startServer (version = '1.16.210', ok) {
|
|||
client.queue('inventory_slot', { window_id: 120, slot: 0, item: new Item().toBedrock() })
|
||||
}
|
||||
|
||||
client.write('player_list', get('player_list'))
|
||||
client.write('start_game', get('start_game'))
|
||||
client.write('item_component', { entries: [] })
|
||||
client.write('set_spawn_position', get('set_spawn_position'))
|
||||
client.write('set_time', { time: 5433771 })
|
||||
client.write('set_difficulty', { difficulty: 1 })
|
||||
client.write('set_commands_enabled', { enabled: true })
|
||||
client.write('adventure_settings', get('adventure_settings'))
|
||||
client.write('biome_definition_list', get('biome_definition_list'))
|
||||
client.write('available_entity_identifiers', get('available_entity_identifiers'))
|
||||
client.write('update_attributes', get('update_attributes'))
|
||||
client.write('creative_content', get('creative_content'))
|
||||
client.write('inventory_content', get('inventory_content'))
|
||||
client.write('player_hotbar', { selected_slot: 3, window_id: 'inventory', select_slot: true })
|
||||
client.write('crafting_data', get('crafting_data'))
|
||||
client.write('available_commands', get('available_commands'))
|
||||
client.write('chunk_radius_update', { chunk_radius: 1 })
|
||||
client.write('game_rules_changed', get('game_rules_changed'))
|
||||
client.write('respawn', get('respawn'))
|
||||
client.queue('player_list', get('player_list'))
|
||||
client.queue('start_game', get('start_game'))
|
||||
client.queue('item_component', { entries: [] })
|
||||
client.queue('set_spawn_position', get('set_spawn_position'))
|
||||
client.queue('set_time', { time: 5433771 })
|
||||
client.queue('set_difficulty', { difficulty: 1 })
|
||||
client.queue('set_commands_enabled', { enabled: true })
|
||||
client.queue('adventure_settings', get('adventure_settings'))
|
||||
client.queue('biome_definition_list', get('biome_definition_list'))
|
||||
client.queue('available_entity_identifiers', get('available_entity_identifiers'))
|
||||
client.queue('update_attributes', get('update_attributes'))
|
||||
client.queue('creative_content', get('creative_content'))
|
||||
client.queue('inventory_content', get('inventory_content'))
|
||||
client.queue('player_hotbar', { selected_slot: 3, window_id: 'inventory', select_slot: true })
|
||||
client.queue('crafting_data', get('crafting_data'))
|
||||
client.queue('available_commands', get('available_commands'))
|
||||
client.queue('chunk_radius_update', { chunk_radius: 1 })
|
||||
client.queue('game_rules_changed', get('game_rules_changed'))
|
||||
client.queue('respawn', get('respawn'))
|
||||
|
||||
for (const chunk of chunks) {
|
||||
client.queue('level_chunk', chunk)
|
||||
|
|
|
|||
111
index.d.ts
vendored
Normal file
111
index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import EventEmitter from "events"
|
||||
|
||||
declare module "bedrock-protocol" {
|
||||
type Version = '1.16.220' | '1.16.210' | '1.16.201'
|
||||
|
||||
export interface Options {
|
||||
// The string version to start the client or server as
|
||||
version: number,
|
||||
// For the client, the hostname of the server to connect to (default: 127.0.0.1)
|
||||
// For the server, the hostname to bind to (default: 0.0.0.0)
|
||||
host: string,
|
||||
// The port to connect or bind to, default: 19132
|
||||
port: number
|
||||
}
|
||||
|
||||
enum ClientStatus {
|
||||
Disconected, Authenticating, Initializing, Initialized
|
||||
}
|
||||
|
||||
export class Connection extends EventEmitter {
|
||||
readonly status: ClientStatus
|
||||
|
||||
// Check if the passed version is less than or greater than the current connected client version.
|
||||
versionLessThan(version: string | number)
|
||||
versionGreaterThan(version: string | number)
|
||||
|
||||
// Writes a Minecraft bedrock packet and sends it without queue batching
|
||||
write(name: string, params: object)
|
||||
// Adds a Minecraft bedrock packet to be sent in the next outgoing batch
|
||||
queue(name: string, params: object)
|
||||
// Writes a MCPE buffer to the connection and skips Protodef serialization. `immediate` if skip queue.
|
||||
sendBuffer(buffer: Buffer, immediate?: boolean)
|
||||
}
|
||||
|
||||
type PlayStatus =
|
||||
| 'login_success'
|
||||
// # Displays "Could not connect: Outdated client!"
|
||||
| 'failed_client'
|
||||
// # Displays "Could not connect: Outdated server!"
|
||||
| 'failed_spawn'
|
||||
// # Sent after world data to spawn the player
|
||||
| 'player_spawn'
|
||||
// # Displays "Unable to connect to world. Your school does not have access to this server."
|
||||
| 'failed_invalid_tenant'
|
||||
// # Displays "The server is not running Minecraft: Education Edition. Failed to connect."
|
||||
| 'failed_vanilla_edu'
|
||||
// # Displays "The server is running an incompatible edition of Minecraft. Failed to connect."
|
||||
| 'failed_edu_vanilla'
|
||||
// # Displays "Wow this server is popular! Check back later to see if space opens up. Server Full"
|
||||
| 'failed_server_full'
|
||||
|
||||
|
||||
export class Client extends Connection {
|
||||
constructor(options: Options)
|
||||
// The client's EntityID returned by the server
|
||||
readonly entityId: BigInt
|
||||
|
||||
/**
|
||||
* Close the connection, leave the server.
|
||||
*/
|
||||
close()
|
||||
}
|
||||
|
||||
/**
|
||||
* `Player` represents a player connected to the server.
|
||||
*/
|
||||
export class Player extends Connection {
|
||||
/**
|
||||
* Disconnects a client before it has logged in via a PlayStatus packet.
|
||||
* @param {string} playStatus
|
||||
*/
|
||||
sendDisconnectStatus(playStatus: PlayStatus)
|
||||
|
||||
/**
|
||||
* Disconnects a client
|
||||
* @param reason The message to be shown to the user on disconnect
|
||||
* @param hide Don't show the client the reason for the disconnect
|
||||
*/
|
||||
disconnect(reason: string, hide?: boolean)
|
||||
|
||||
/**
|
||||
* Close the connection. Already called by disconnect. Call this to manually close RakNet connection.
|
||||
*/
|
||||
close()
|
||||
}
|
||||
|
||||
export class Server extends EventEmitter {
|
||||
clients: Map<string, Player>
|
||||
constructor(options: Options)
|
||||
// Disconnects all currently connected clients
|
||||
close(disconnectReason: string)
|
||||
}
|
||||
|
||||
class ServerAdvertisement {
|
||||
motd: string
|
||||
name: string
|
||||
protocol: number
|
||||
version: string
|
||||
players: {
|
||||
online: number,
|
||||
max: number
|
||||
}
|
||||
gamemode: string
|
||||
serverId: string
|
||||
}
|
||||
|
||||
export function createClient(options: Options): Client
|
||||
export function createServer(options: Options): Server
|
||||
|
||||
export function ping({ host, port }) : ServerAdvertisement
|
||||
}
|
||||
3
index.js
3
index.js
|
|
@ -3,5 +3,6 @@ module.exports = {
|
|||
...require('./src/server'),
|
||||
...require('./src/serverPlayer'),
|
||||
...require('./src/relay'),
|
||||
...require('./src/createClient')
|
||||
...require('./src/createClient'),
|
||||
...require('./src/createServer')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ module.exports = (client, server, options) => {
|
|||
// inside of PlayFab.
|
||||
PlayFabId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0', // 1.16.210
|
||||
PremiumSkin: false,
|
||||
SelfSignedId: '78eb38a6-950e-3ab9-b2cf-dd849e343702',
|
||||
SelfSignedId: nextUUID(),
|
||||
ServerAddress: `${options.hostname}:${options.port}`,
|
||||
SkinAnimationData: '',
|
||||
SkinColor: '#ffffcd96',
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ module.exports = (client, server, options) => {
|
|||
// from Xbox with addition user profile data
|
||||
// We verify that at least one of the tokens in the chain has been properly
|
||||
// signed by Mojang by checking the x509 public key in the JWT headers
|
||||
// let didVerify = false
|
||||
let didVerify = false
|
||||
|
||||
let pubKey = mcPubKeyToPem(getX5U(chain[0])) // the first one is client signed, allow it
|
||||
let finalKey = null
|
||||
|
|
@ -26,17 +26,20 @@ module.exports = (client, server, options) => {
|
|||
// Check if signed by Mojang key
|
||||
const x5u = getX5U(token)
|
||||
if (x5u === constants.PUBLIC_KEY && !data.extraData?.XUID) {
|
||||
// didVerify = true
|
||||
didVerify = true
|
||||
debug('Verified client with mojang key', x5u)
|
||||
}
|
||||
|
||||
// TODO: Handle `didVerify` = false
|
||||
pubKey = decoded.identityPublicKey ? mcPubKeyToPem(decoded.identityPublicKey) : x5u
|
||||
finalKey = decoded.identityPublicKey || finalKey // non pem
|
||||
data = { ...data, ...decoded }
|
||||
}
|
||||
// console.log('Result', data)
|
||||
|
||||
if (!didVerify && !options.offline) {
|
||||
client.disconnect('disconnectionScreen.notAuthenticated')
|
||||
}
|
||||
|
||||
return { key: finalKey, data }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,25 +29,30 @@ class Client extends Connection {
|
|||
Login(this, null, this.options)
|
||||
LoginVerify(this, null, this.options)
|
||||
|
||||
this.on('session', this.connect)
|
||||
|
||||
if (options.offline) {
|
||||
console.debug('offline mode, not authenticating', this.options)
|
||||
auth.createOfflineSession(this, this.options)
|
||||
} else if (options.password) {
|
||||
auth.authenticatePassword(this, this.options)
|
||||
} else {
|
||||
auth.authenticateDeviceCode(this, this.options)
|
||||
}
|
||||
const hostname = this.options.hostname
|
||||
const port = this.options.port
|
||||
this.connection = new RakClient({ useWorkers: true, hostname, port })
|
||||
|
||||
this.startGameData = {}
|
||||
this.clientRuntimeId = null
|
||||
|
||||
this.startQueue()
|
||||
this.inLog = (...args) => debug('C ->', ...args)
|
||||
this.outLog = (...args) => debug('C <-', ...args)
|
||||
}
|
||||
|
||||
connect () {
|
||||
this.on('session', this._connect)
|
||||
|
||||
if (this.options.offline) {
|
||||
console.debug('offline mode, not authenticating', this.options)
|
||||
auth.createOfflineSession(this, this.options)
|
||||
} else {
|
||||
auth.authenticateDeviceCode(this, this.options)
|
||||
}
|
||||
|
||||
this.startQueue()
|
||||
}
|
||||
|
||||
validateOptions () {
|
||||
if (!this.options.hostname || this.options.port == null) throw Error('Invalid hostname/port')
|
||||
|
||||
|
|
@ -70,18 +75,23 @@ class Client extends Connection {
|
|||
this.handle(buffer)
|
||||
}
|
||||
|
||||
connect = async (sessionData) => {
|
||||
const hostname = this.options.hostname
|
||||
const port = this.options.port
|
||||
debug('[client] connecting to', hostname, port, sessionData)
|
||||
async ping () {
|
||||
try {
|
||||
return await this.connection.ping(this.options.connectTimeout)
|
||||
} catch (e) {
|
||||
console.warn(`Unable to connect to [${this.options.hostname}]/${this.options.port}. Is the server running?`)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
this.connection = new RakClient({ useWorkers: true, hostname, port })
|
||||
_connect = async (sessionData) => {
|
||||
debug('[client] connecting to', this.options.hostname, this.options.port, sessionData, this.connection)
|
||||
this.connection.onConnected = () => this.sendLogin()
|
||||
this.connection.onCloseConnection = () => this.close()
|
||||
this.connection.onEncapsulated = this.onEncapsulated
|
||||
this.connection.connect()
|
||||
|
||||
this.connectTimer = setTimeout(() => {
|
||||
this.connectTimeout = setTimeout(() => {
|
||||
if (this.status === ClientStatus.Disconnected) {
|
||||
this.connection.close()
|
||||
this.emit('error', 'connect timed out')
|
||||
|
|
@ -115,6 +125,7 @@ class Client extends Connection {
|
|||
onDisconnectRequest (packet) {
|
||||
console.warn(`C Server requested ${packet.hide_disconnect_reason ? 'silent disconnect' : 'disconnect'}: ${packet.message}`)
|
||||
this.emit('kick', packet)
|
||||
this.close()
|
||||
}
|
||||
|
||||
onPlayStatus (statusPacket) {
|
||||
|
|
@ -128,21 +139,24 @@ class Client extends Connection {
|
|||
}
|
||||
|
||||
close () {
|
||||
this.emit('close')
|
||||
if (this.status !== ClientStatus.Disconnected) {
|
||||
this.emit('close') // Emit close once
|
||||
console.log('Client closed!')
|
||||
}
|
||||
clearInterval(this.loop)
|
||||
clearTimeout(this.connectTimeout)
|
||||
this.q = []
|
||||
this.q2 = []
|
||||
this.connection?.close()
|
||||
this.removeAllListeners()
|
||||
console.log('Client closed!')
|
||||
this.status = ClientStatus.Disconnected
|
||||
}
|
||||
|
||||
tryRencode (name, params, actual) {
|
||||
const packet = this.serializer.createPacketBuffer({ name, params })
|
||||
|
||||
console.assert(packet.toString('hex') === actual.toString('hex'))
|
||||
if (packet.toString('hex') !== actual.toString('hex')) {
|
||||
console.assert(packet.equals(actual))
|
||||
if (!packet.equals(actual)) {
|
||||
const ours = packet.toString('hex').match(/.{1,16}/g).join('\n')
|
||||
const theirs = actual.toString('hex').match(/.{1,16}/g).join('\n')
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class Connection extends EventEmitter {
|
|||
if (this.q.length) {
|
||||
// TODO: can we just build Batch before the queue loop?
|
||||
const batch = new BatchPacket()
|
||||
this.outLog('<- BATCH', this.q2)
|
||||
this.outLog('<- Batch', this.q2)
|
||||
const sending = []
|
||||
for (let i = 0; i < this.q.length; i++) {
|
||||
const packet = this.q.shift()
|
||||
|
|
|
|||
|
|
@ -1,45 +1,76 @@
|
|||
const { Client } = require('./client')
|
||||
const { RakClient } = require('./rak')
|
||||
const assert = require('assert')
|
||||
const advertisement = require('./server/advertisement')
|
||||
|
||||
module.exports = { createClient }
|
||||
|
||||
/** @param {{ version?: number, hostname: string, port?: number }} options */
|
||||
/** @param {{ version?: number, hostname: string, port?: number, connectTimeout?: number }} options */
|
||||
function createClient (options) {
|
||||
assert(options && options.hostname)
|
||||
assert(options)
|
||||
if (options.host) options.hostname = options.host
|
||||
const client = new Client({ port: 19132, ...options })
|
||||
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
handleResourcePackInfo(client)
|
||||
disableClientCache(client)
|
||||
handleRenderDistance(client)
|
||||
handleTickSync(client)
|
||||
})
|
||||
if (options.skipPing) {
|
||||
connect(client)
|
||||
} else { // Try to ping
|
||||
client.ping().then(data => {
|
||||
const advert = advertisement.fromServerName(data)
|
||||
console.log(`Connecting to server ${advert.motd} (${advert.name}), version ${advert.version}`)
|
||||
// TODO: update connect version based on ping response
|
||||
connect(client)
|
||||
}, client)
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
function handleResourcePackInfo (client) {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
function connect (client) {
|
||||
// Actually connect
|
||||
client.connect()
|
||||
|
||||
client.once('resource_pack_stack', (stack) => {
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_stack', (stack) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
})
|
||||
|
||||
client.queue('client_cache_status', { enabled: false })
|
||||
client.queue('request_chunk_radius', { chunk_radius: client.renderDistance || 1 })
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
|
||||
})
|
||||
|
||||
const KEEPALIVE_INTERVAL = 10 // Send tick sync packets every 10 ticks
|
||||
let keepalive
|
||||
client.tick = 0n
|
||||
client.once('spawn', () => {
|
||||
keepalive = setInterval(() => {
|
||||
// Client fills out the request_time and the server does response_time in its reply.
|
||||
client.queue('tick_sync', { request_time: client.tick, response_time: 0n })
|
||||
client.tick += BigInt(KEEPALIVE_INTERVAL)
|
||||
}, 50 * KEEPALIVE_INTERVAL)
|
||||
|
||||
client.on('tick_sync', async packet => {
|
||||
client.emit('heartbeat', packet.response_time)
|
||||
client.tick = packet.response_time
|
||||
})
|
||||
})
|
||||
|
||||
client.once('close', () => {
|
||||
clearInterval(keepalive)
|
||||
})
|
||||
}
|
||||
|
||||
function handleRenderDistance (client) {
|
||||
client.queue('request_chunk_radius', { chunk_radius: 1 })
|
||||
async function ping ({ host, port }) {
|
||||
const con = new RakClient({ hostname: host, port })
|
||||
const ret = await con.ping()
|
||||
con.close()
|
||||
return advertisement.fromServerName(ret)
|
||||
}
|
||||
|
||||
function disableClientCache (client) {
|
||||
client.queue('client_cache_status', { enabled: false })
|
||||
}
|
||||
|
||||
function handleTickSync (client) {
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
|
||||
}
|
||||
module.exports = { createClient, ping }
|
||||
|
|
|
|||
11
src/createServer.js
Normal file
11
src/createServer.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const { Server } = require('./server')
|
||||
|
||||
function createServer (options) {
|
||||
if (options.host) options.hostname = options.host
|
||||
if (!options.port) options.port = 19132
|
||||
const server = new Server(options)
|
||||
server.listen()
|
||||
return server
|
||||
}
|
||||
|
||||
module.exports = { createServer }
|
||||
|
|
@ -36,7 +36,7 @@ class RakNativeClient extends EventEmitter {
|
|||
})
|
||||
}
|
||||
|
||||
async ping () {
|
||||
async ping (timeout = 1000) {
|
||||
this.raknet.ping()
|
||||
return waitFor((done) => {
|
||||
this.raknet.on('pong', (ret) => {
|
||||
|
|
@ -44,7 +44,7 @@ class RakNativeClient extends EventEmitter {
|
|||
done(ret.extra.toString())
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
}, timeout, () => { throw new Error('Ping timed out') })
|
||||
}
|
||||
|
||||
connect () {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const { EventEmitter } = require('events')
|
|||
const { createDeserializer, createSerializer } = require('./transforms/serializer')
|
||||
const { Player } = require('./serverPlayer')
|
||||
const { RakServer } = require('./rak')
|
||||
const { sleep } = require('./datatypes/util')
|
||||
const Options = require('./options')
|
||||
const debug = globalThis.isElectron ? console.debug : require('debug')('minecraft-protocol')
|
||||
|
||||
|
|
@ -31,7 +32,7 @@ class Server extends EventEmitter {
|
|||
}
|
||||
|
||||
onOpenConnection = (conn) => {
|
||||
this.inLog('new connection', conn)
|
||||
console.debug('new connection', conn?.address)
|
||||
const player = new Player(this, conn)
|
||||
this.clients[conn.address] = player
|
||||
this.clientCount++
|
||||
|
|
@ -39,7 +40,7 @@ class Server extends EventEmitter {
|
|||
}
|
||||
|
||||
onCloseConnection = (inetAddr, reason) => {
|
||||
console.debug('close connection', inetAddr, reason)
|
||||
console.debug('close connection', inetAddr?.address, reason)
|
||||
delete this.clients[inetAddr]?.connection // Prevent close loop
|
||||
this.clients[inetAddr]?.close()
|
||||
delete this.clients[inetAddr]
|
||||
|
|
@ -57,7 +58,12 @@ class Server extends EventEmitter {
|
|||
|
||||
async listen (hostname = this.options.hostname, port = this.options.port) {
|
||||
this.raknet = new RakServer({ hostname, port })
|
||||
await this.raknet.listen()
|
||||
try {
|
||||
await this.raknet.listen()
|
||||
} catch (e) {
|
||||
console.warn(`Failed to bind server on [${this.options.hostname}]/${this.options.port}, is the port free?`)
|
||||
throw e
|
||||
}
|
||||
console.debug('Listening on', hostname, port)
|
||||
this.raknet.onOpenConnection = this.onOpenConnection
|
||||
this.raknet.onCloseConnection = this.onCloseConnection
|
||||
|
|
@ -65,14 +71,18 @@ class Server extends EventEmitter {
|
|||
return { hostname, port }
|
||||
}
|
||||
|
||||
close (disconnectReason) {
|
||||
async close (disconnectReason) {
|
||||
for (const caddr in this.clients) {
|
||||
const client = this.clients[caddr]
|
||||
client.disconnect(disconnectReason)
|
||||
}
|
||||
this.raknet.close()
|
||||
|
||||
this.clients = {}
|
||||
this.clientCount = 0
|
||||
|
||||
// Allow some time for client to get disconnect before closing connection.
|
||||
await sleep(60)
|
||||
this.raknet.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
const { Versions, CURRENT_VERSION } = require('../options')
|
||||
|
||||
class ServerName {
|
||||
motd = 'Bedrock Protocol Server'
|
||||
name = 'bedrock-protocol'
|
||||
protocol = 408
|
||||
version = '1.16.20'
|
||||
protocol = Versions[CURRENT_VERSION]
|
||||
version = CURRENT_VERSION
|
||||
players = {
|
||||
online: 0,
|
||||
max: 5
|
||||
|
|
@ -11,6 +13,14 @@ class ServerName {
|
|||
gamemode = 'Creative'
|
||||
serverId = '0'
|
||||
|
||||
fromString (str) {
|
||||
const [header, motd, protocol, version, playersOnline, playersMax, serverId, name, gamemode] = str.split(';')
|
||||
if (playersOnline) this.players.online = playersOnline
|
||||
if (playersMax) this.players.max = playersMax
|
||||
Object.assign(this, { header, motd, protocol, version, serverId, name, gamemode })
|
||||
return this
|
||||
}
|
||||
|
||||
toString (version) {
|
||||
return [
|
||||
'MCPE',
|
||||
|
|
@ -35,5 +45,8 @@ module.exports = {
|
|||
ServerName,
|
||||
getServerName (client) {
|
||||
return new ServerName().toBuffer()
|
||||
},
|
||||
fromServerName (string) {
|
||||
return new ServerName().fromString(string)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class Player extends Connection {
|
|||
const clientVer = body.protocol_version
|
||||
if (this.server.options.protocolVersion) {
|
||||
if (this.server.options.protocolVersion < clientVer) {
|
||||
this.sendDisconnectStatus('failed_client')
|
||||
this.sendDisconnectStatus('failed_spawn')
|
||||
return
|
||||
}
|
||||
} else if (clientVer < Options.MIN_VERSION) {
|
||||
|
|
@ -76,23 +76,22 @@ class Player extends Connection {
|
|||
* @param {string} playStatus
|
||||
*/
|
||||
sendDisconnectStatus (playStatus) {
|
||||
if (this.status === ClientStatus.Disconnected) return
|
||||
this.write('play_status', { status: playStatus })
|
||||
this.close()
|
||||
this.close('kick')
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects a client
|
||||
*/
|
||||
disconnect (reason = 'Server closed', hide = false) {
|
||||
if ([ClientStatus.Authenticating, ClientStatus.Initializing].includes(this.status)) {
|
||||
this.sendDisconnectStatus('failed_server_full')
|
||||
} else {
|
||||
this.write('disconnect', {
|
||||
hide_disconnect_screen: hide,
|
||||
message: reason
|
||||
})
|
||||
}
|
||||
this.close()
|
||||
if (this.status === ClientStatus.Disconnected) return
|
||||
this.write('disconnect', {
|
||||
hide_disconnect_screen: hide,
|
||||
message: reason
|
||||
})
|
||||
console.debug('Kicked ', this.connection?.address, reason)
|
||||
setTimeout(() => this.close('kick'), 100) // Allow time for message to be recieved.
|
||||
}
|
||||
|
||||
// After sending Server to Client Handshake, this handles the client's
|
||||
|
|
@ -105,7 +104,11 @@ class Player extends Connection {
|
|||
this.emit('join')
|
||||
}
|
||||
|
||||
close () {
|
||||
close (reason) {
|
||||
if (this.status !== ClientStatus.Disconnected) {
|
||||
this.emit('close') // Emit close once
|
||||
if (!reason) console.trace('Client closed connection', this.connection?.address)
|
||||
}
|
||||
this.q = []
|
||||
this.q2 = []
|
||||
clearInterval(this.loop)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
const { Server, Client } = require('../')
|
||||
const { dumpPackets } = require('../tools/genPacketDumps')
|
||||
const DataProvider = require('../data/provider')
|
||||
const { ping } = require('../src/createClient')
|
||||
|
||||
// First we need to dump some packets that a vanilla server would send a vanilla
|
||||
// client. Then we can replay those back in our custom server.
|
||||
|
|
@ -9,11 +10,11 @@ function prepare (version) {
|
|||
return dumpPackets(version)
|
||||
}
|
||||
|
||||
async function startTest (version = '1.16.201', ok) {
|
||||
async function startTest (version = '1.16.220', ok) {
|
||||
await prepare(version)
|
||||
const Item = require('../types/Item')(version)
|
||||
const port = 19130
|
||||
const server = new Server({ hostname: '0.0.0.0', port, version })
|
||||
const server = new Server({ hostname: '0.0.0.0', port, version, offline: true })
|
||||
|
||||
function getPath (packetPath) {
|
||||
return DataProvider(server.options.protocolVersion).getPath(packetPath)
|
||||
|
|
@ -26,6 +27,9 @@ async function startTest (version = '1.16.201', ok) {
|
|||
server.listen()
|
||||
console.log('Started server')
|
||||
|
||||
const pongData = await ping({ host: '127.0.0.1', port })
|
||||
console.assert(pongData, 'did not get valid pong data from server')
|
||||
|
||||
const respawnPacket = get('packets/respawn.json')
|
||||
const chunks = await requestChunks(respawnPacket.x, respawnPacket.z, 1)
|
||||
|
||||
|
|
@ -136,11 +140,14 @@ async function startTest (version = '1.16.201', ok) {
|
|||
setTimeout(() => {
|
||||
client.close()
|
||||
|
||||
server.close()
|
||||
ok?.()
|
||||
server.close().then(() => {
|
||||
ok?.()
|
||||
})
|
||||
}, 500)
|
||||
clearInterval(loop)
|
||||
})
|
||||
|
||||
client.connect()
|
||||
}
|
||||
|
||||
const { ChunkColumn, Version } = require('bedrock-provider')
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ const { timedTest } = require('./internal')
|
|||
const { Versions } = require('../src/options')
|
||||
|
||||
describe('internal client/server test', function () {
|
||||
this.timeout(120 * 1000)
|
||||
this.timeout(220 * 1000)
|
||||
|
||||
it('connects', async () => {
|
||||
for (const version in Versions) {
|
||||
for (const version in Versions) {
|
||||
it('connects ' + version, async () => {
|
||||
console.debug(version)
|
||||
await timedTest(version)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ async function test (version) {
|
|||
})
|
||||
|
||||
console.log('Started client')
|
||||
client.connect()
|
||||
|
||||
let loop
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const { clientTest } = require('./vanilla')
|
|||
const { Versions } = require('../src/options')
|
||||
|
||||
describe('vanilla server test', function () {
|
||||
this.timeout(120 * 1000)
|
||||
this.timeout(220 * 1000)
|
||||
|
||||
for (const version in Versions) {
|
||||
it('client spawns ' + version, async () => {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ async function dump (version, force) {
|
|||
username: 'Boat' + random,
|
||||
offline: true
|
||||
})
|
||||
|
||||
client.connect()
|
||||
return waitFor(async res => {
|
||||
const root = join(__dirname, `../data/${client.options.version}/sample/`)
|
||||
if (!fs.existsSync(root + 'packets') || !fs.existsSync(root + 'chunks')) {
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ async function startServerAndWait (version, withTimeout, options) {
|
|||
|
||||
if (!module.parent) {
|
||||
// if (process.argv.length < 3) throw Error('Missing version argument')
|
||||
startServer(process.argv[2] || '1.16.201', null, process.argv[3] ? { 'server-port': process.argv[3] } : undefined)
|
||||
startServer(process.argv[2] || '1.16.201', null, process.argv[3] ? { 'server-port': process.argv[3], 'online-mode': !!process.argv[4] } : undefined)
|
||||
}
|
||||
|
||||
module.exports = { fetchLatestStable, startServer, startServerAndWait }
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ module.exports = (version) =>
|
|||
network_id: this.networkId,
|
||||
count: this.count,
|
||||
metadata: this.metadata,
|
||||
has_stack_id: this.stackId,
|
||||
stack_id: this.stackId,
|
||||
extra: {
|
||||
has_nbt: !!this.nbt,
|
||||
nbt: { version: 1, nbt: this.nbt },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue