diff --git a/src/client.js b/src/client.js new file mode 100644 index 0000000..8fcea88 --- /dev/null +++ b/src/client.js @@ -0,0 +1,110 @@ +'use strict'; + +var EventEmitter = require('events').EventEmitter; +var debug = require('./debug'); + +var createSerializer = require("./transforms/serializer").createSerializer; +var createDeserializer = require("./transforms/serializer").createDeserializer; + +class Client extends EventEmitter { + + constructor(isServer) { + super(); + this.isServer = !!isServer; + } + + setSerializer() { + this.serializer = createSerializer(this.isServer); + this.deserializer = createDeserializer(this.isServer); + + + this.serializer.on('error', (e) => { + var parts = e.field.split("."); + parts.shift(); + e.message = `Serialization error for ${e.message}`; + this.emit('error', e); + }); + + + this.deserializer.on('error', (e) => { + var parts = e.field.split("."); + parts.shift(); + e.message = `Deserialization error for ${e.message}`; + this.emit('error', e); + }); + + this.deserializer.on('data', (parsed) => { + parsed.metadata.name = parsed.data.name; + parsed.data = parsed.data.params; + this.emit('packet', parsed.data, parsed.metadata); + + debug("reading packet " + "." + parsed.metadata.name); + debug(parsed.data); + this.emit(parsed.metadata.name, parsed.data, parsed.metadata); + this.emit('raw.' + parsed.metadata.name, parsed.buffer, parsed.metadata); + this.emit('raw', parsed.buffer, parsed.metadata); + }); + } + + setSocket(socket) { + var ended = false; + + var endSocket = function() { + if (ended) return; + ended = true; + this.socket.removeListener('close', endSocket); + this.socket.removeListener('end', endSocket); + this.socket.removeListener('timeout', endSocket); + this.emit('end', this._endReason); + }; + + var onFatalError = function(err) { + this.emit('error', err); + endSocket(); + }; + + var onError = function(err) { + this.emit('error', err); + } + + this.socket = socket; + + if (this.socket.setNoDelay) { + this.socket.setNoDelay(true); + } + + this.socket.on('connect', function() { + this.emit('connect'); + }); + + this.socket.on('error', onFatalError); + this.socket.on('close', endSocket); + this.socket.on('end', endSocket); + this.socket.on('timeout', endSocket); + + this.setSerializer(); + this.socket.pipe(this.deserializer); + this.serializer.pipe(this.socket); + } + + end(reason) { + this._endReason = reason; + if (this.socket) + this.socket.end(); + } + + write(name, params) { + debug("writing packet " + "." + name); + debug(params); + this.serializer.write({ + name, + params + }); + } + + writeRaw(buffer) { + this.socket.write(buffer); + } +} + +module.exports = Client; diff --git a/src/createClient.js b/src/createClient.js new file mode 100644 index 0000000..47b81fe --- /dev/null +++ b/src/createClient.js @@ -0,0 +1,42 @@ +'use strict'; + +var net = require('net'); +var dns = require('dns'); +var Client = require('./client'); +var assert = require('assert'); + +module.exports = createClient; + +Client.prototype.connect = function(port, host) { + var self = this; + if(port == 19132 && net.isIP(host) === 0) { + dns.resolveSrv(host, function(err, addresses) { + if(addresses && addresses.length > 0) { + self.setSocket(net.connect(addresses[0].port, addresses[0].name)); + } else { + self.setSocket(net.connect(port, host)); + } + }); + } else { + self.setSocket(net.connect(port, host)); + } +}; + +function createClient(options) { + assert.ok(options, "options is required"); + var port = options.port || 19132; + var host = options.host || 'localhost'; + + assert.ok(options.username, "username is required"); + + + var client = new Client(false); + client.on('connect', onConnect); + client.username = options.username; + client.connect(port, host); + function onConnect() { + // we should probably implement the login protocol for the client here + } + + return client; +} diff --git a/src/createServer.js b/src/createServer.js new file mode 100644 index 0000000..5c789d6 --- /dev/null +++ b/src/createServer.js @@ -0,0 +1,68 @@ +var Server = require('./server'); + +function createServer(options) { + options = options || {}; + var port = options.port != null ? + options.port : + options['server-port'] != null ? + options['server-port'] : + 19132; + var host = options.host || '0.0.0.0'; + var kickTimeout = options.kickTimeout || 10 * 1000; + var checkTimeoutInterval = options.checkTimeoutInterval || 4 * 1000; + var onlineMode = options['online-mode'] == null ? true : options['online-mode']; + var beforePing = options.beforePing || null; + var enablePing = options.ping == null ? true : options.ping; + + var server = new Server(); + + server.name = options.name || "Minecraft Server"; + server.motd = options.motd || "A Minecraft server"; + server.maxPlayers = options['max-players'] || 20; + server.playerCount = 0; + server.onlineModeExceptions = {}; + + server.on("connection", function (client) { + client.once('player_identification', onLogin); + client.on('end', onEnd); + + var ping = true; + var pingTimer = null; + + function pingLoop() { + client.write('ping', {}); + } + + function startPing() { + pingTimer = setInterval(pingLoop, checkTimeoutInterval); + } + + function onEnd() { + clearInterval(pingTimer); + } + + function onLogin(packet) { + client.username=packet.username; + client.identification_byte=packet.unused; + + if(options.handshake) + { + options.handshake(function(){ + continueLogin(); + }) + } + else + continueLogin(); + } + + function continueLogin() { + // we should probably implement the login protocol here + server.emit('login', client); + startPing(); + } + }); + server.listen(port, host); + return server; +} + +module.exports = createServer; diff --git a/src/datatypes.js b/src/datatypes/minecraft.js similarity index 100% rename from src/datatypes.js rename to src/datatypes/minecraft.js diff --git a/src/debug.js b/src/debug.js new file mode 100644 index 0000000..703e2e1 --- /dev/null +++ b/src/debug.js @@ -0,0 +1,18 @@ +var util = require('util') + +var debug; +if(process.env.NODE_DEBUG) { + var pid = process.pid; + debug = function(x) { + // if console is not set up yet, then skip this. + if(!console.error) + return; + console.error('MC-PROTO: %d', pid, + util.format.apply(util, arguments)); + }; +} else { + debug = function() { + }; +} + +module.exports = debug; diff --git a/src/index.js b/src/index.js index 2763984..a1d6391 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,6 @@ module.exports = { - createDeserializer: require('./serializer').createDeserializer, - createSerializer: require('./serializer').createSerializer -} + createSerializer: require("./transforms/serializer").createSerializer, + createDeserializer: require("./transforms/serializer").createDeserializer, + createServer: require("./createServer"), + createClient: require("./createClient") +}; diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..d931220 --- /dev/null +++ b/src/server.js @@ -0,0 +1,58 @@ +'use strict'; + +var net = require('net'); +var EventEmitter = require('events').EventEmitter; +var Client = require('./client'); + +class Server extends EventEmitter { + + constructor() { + super(); + } + + listen(port, host) { + var self = this; + var nextId = 0; + self.socketServer = net.createServer(); + self.socketServer.on('connection', socket => { + var client = new Client(true); + // client._end = client.end; + // client.end = function end(endReason) { + // client.write('disconnect_player', { + // disconnect_reason: endReason + // }); + // client._end(endReason); + // }; + client.id = nextId++; + self.clients[client.id] = client; + client.on('end', function () { + delete self.clients[client.id]; + }); + client.setSocket(socket); + self.emit('connection', client); + }); + self.socketServer.on('error', function (err) { + self.emit('error', err); + }); + self.socketServer.on('close', function () { + self.emit('close'); + }); + self.socketServer.on('listening', function () { + self.emit('listening'); + }); + self.socketServer.listen(port, host); + } + + close() { + var client; + for (var clientId in this.clients) { + if (!this.clients.hasOwnProperty(clientId)) continue; + + client = this.clients[clientId]; + client.end('ServerShutdown'); + } + this.socketServer.close(); + } +} + +module.exports = Server; diff --git a/src/serializer.js b/src/transforms/serializer.js similarity index 81% rename from src/serializer.js rename to src/transforms/serializer.js index cbd1e87..892eba5 100644 --- a/src/serializer.js +++ b/src/transforms/serializer.js @@ -2,11 +2,11 @@ var ProtoDef = require('protodef').ProtoDef; var Serializer = require('protodef').Serializer; var Parser = require('protodef').Parser; -var protocol = require('../data/protocol.json').types; +var protocol = require(__dirname + '/../../data/protocol.json').types; function createProtocol(packets) { var proto = new ProtoDef(); - proto.addTypes(require('./datatypes')); + proto.addTypes(require('../datatypes/minecraft')); return proto; }