/tera-network-proxy

Modular event handling through a TERA proxy server.

Primary LanguageJavaScriptGNU General Public License v3.0GPL-3.0

tera-network-proxy

Hosts a TCP proxy server to read, modify, and inject network data between a TERA game client and server. This modular system built on event-based hooks allows for easy creation and usage of script mods.

This module is primarily intended for use in tera-proxy.

This document is divided into two sections. Most readers are likely viewing this for documentation on writing modules, so the module reference will come first. The latter half, the API reference, details the classes exported by require('tera-network-proxy'), which is not needed for module authors.

Module Reference

A module loaded through dispatch.load(name) is instantiated similarly to:

const YourModule = require(name);
modules[name] = new YourModule(dispatch);

Thus, a loadable module's export must be a constructible function with at least one parameter, which will be an instance of Module. Since this function is called with new, the context of this will be unique to each connection to the proxy allowing support for multiple clients.

For examples, see the tera-proxy developer documentation.

Module

An instance of Module is created for each module loaded by Dispatch. It keeps track of a module's name and has a handful of methods which mostly forward parameters to the base Dispatch instance. It is often named mod for conciseness. In general, it is discouraged to interact with the base Dispatch object.

Properties

dispatch

Points to the base Dispatch instance. Useful if you want to track something per-connection given an instance of a Module, where you can make a WeakMap with mod.dispatch as a key.

Methods

hook(name, version, [options], callback)

hookOnce(name, version, [options], callback)

Adds a hook for a packet. The hookOnce version will remove the hook after it has run once.

name will usually be the name of the message being watched for, such as "S_LOGIN", but it can also be "*" to catch all messages. You may also use lowerCamelCase for names, removing all underscores and using lowercase unless the character was originally preceded by an underscore (such as "sLogin").

version should be an integer representing the desired packet version as given by the corresponding tera-data definition file, or it can be "*" to use the latest version. Alternatively, "raw" can be used to create a raw hook, which does not perform any parsing and instead passes the callback the raw (decrypted) data buffer.

options is an optional object with the following optional properties:

  • order: A lower number causes this hook to run before hooks with a higher order. This defaults to 0, so you can imagine negative values running closer to the source side and positive values running closer to the destination side. This is helpful for modules that way want to examine packets before another module modifies or silences them, or filter to what the receiving side will see by running later than most other hooks.

  • filter: An object of filters to apply. The hook will not receive any packets which do not match the filter. Each filter is a ternary flag: true applies the positive filter, false applies it negatively, and null disables the filter. All of them are optional.

    • fake: Filters "fake" packets—those generated through Dispatch. Default: false (only allow real packets). If true, matches only packets generated through Dispatch. Any hook that receives fake packets must be careful not to create an infinite loop.

    • incoming: Filters packets based on destination. Default: null (ignore destination). If true, matches only packets sent to the client. If false, matches only packets sent to the server.

    • modified: Filters packets if they have been altered by a previous hook. Default: null (ignore modification status). If true, matches only packets modified by previous hooks. If false, matches only unmodified packets.

    • silenced: Filters packets if they have been silenced by a previous hook. Default: false (only allow unsilenced packets). If true, matches only packets that have had a previous hook return false to prevent the packet from being sent. Any hook that receives a silenced packet may un-silence the packet by returning true.

callback is the callback function for the hook, which receives:

  • For a normal hook (version is not "raw"),
    • event: An object containing the parsed message data (see tera-data).
    • fake: true if this packet was generated through the proxy, false otherwise.
    • Return value is true if event is modified, or false to stop and silence the message. Other return values are ignored. If you change event but do not return true, the changes will not be saved.
  • For a raw hook,
    • code: The opcode of the message as an integer.
    • data: The Buffer of the raw message data.
    • fromServer: true if the message was sent by the server, false otherwise.
    • fake: true if this packet was generated through the proxy, false otherwise.
    • Return value is a Buffer of the modified message data to use, or false to stop and silence the message. Other return values are ignored. Note that unlike for normal hooks, modifications to the original buffer will be saved and used regardless of whether you return true.

The event for a normal hook and the data for a raw hook also have four properties corresponding to the four filters. Each flag is either true or false; see the above section on filters for more information.

  • $fake
  • $incoming
  • $modified
  • $silenced

Returns an object representing the hook. Its properties are not set in stone and are not meant to be changed, so do not depend on them. If you have a use case where you absolutely need to do something with the properties, please submit a GitHub issue describing it.

unhook(hook)

Removes a hook. It will no longer be called. Pass in the result of a call to Dispatch#hook().

toClient(buffer)

toClient(name, version, data)

toServer(buffer)

toServer(name, version, data)

Constructs and sends a packet to either the TERA client or server.

If buffer is used, it will simply be sent as-is (before encryption).

If data is used, it must be an object (to be serialized by tera-data-parser). name must be the packet name and version should be the version number as given by the tera-data definition file, or "*" for the latest version.

Dispatch

An instance of Dispatch is created for every connection to the proxy game server. You'll typically only need to access this if you are writing for the proxy, not for a module.

Properties

connection

Methods

hook(name, version, [options], callback)

unhook(hook)

Same as the methods of the same name from Module. You generally shouldn't use this hook() since it will be missing module information and requires an explicit unhook().

write(outgoing, buffer)

write(outgoing, name, version, data)

Sends data to one of the associated connections.

outgoing should be true to send to the server, otherwise it will be sent to the client.

For the rest of the parameters, see Module#toClient().

handle(data, fromServer, [fake])

Passes the (usually parsed) data all registered hooks.

Return value is the raw data buffer, or false if silenced by any hook.

reset()

Unloads all modules and removes all hooks.

API Reference

Note: If you are a module author, you most likely do not need anything from this section and may stop reading.

tera-network-proxy exposes three classes, each of which can be accessed as a property of the same name on the exported object. These allow for setting up the proxy game server and hooking up a client to it.

Example

const net = require('net');
const { Connection, RealClient } = require('tera-network-proxy');

// Create a regular TCP server.
const server = net.createServer((socket) => {
  // Disable packet buffering to minimize latency.
  socket.setNoDelay(true);

  // Set up a connection handler.
  const connection = new Connection();

  // Set up a client handler to handle communication between the client and proxy.
  const client = new RealClient(connection, socket);

  // Connect this proxy to some TERA server.
  const proxied = connection.connect(client, { host: '208.67.49.92', port: 10001 });

  // Load the `logger` module.
  connection.dispatch.load('logger');
});

// Listen on localhost:9247.
server.listen(9247, '127.0.0.1', () => {
  const { address, port } = server.address();
  console.log(`listening on ${address}:${port}`);
});

Connection

This class handles encryption and packet buffering for a socket to a real game server.

new Connection()

Sets up a connection handler. This also initializes an instance of Dispatch unique to this Connection.

Properties

dispatch

An intsance of Dispatch.

Methods

connect(client, options)

Start a connection between the real game server and client, either a RealClient or a FakeClient.

options will be passed directly to net.connect.

Returns the net.Socket to the real game server.

close()

Ends the connection and calls Dispatch#reset().

RealClient

A RealClient is for legitimate game clients or otherwise clients driven by outside means. It has its own packet buffer and encryption state to appear transparent to the client while still allowing Dispatch to read, modify, and inject packets.

new RealClient(connection, socket)

connection must be an instance of Connection.

socket must be a TCP socket (see net.Socket). You most likely want to call socket.setNoDelay(true) to disable buffering, or else the client connection will have inconsistent latency.

FakeClient

A FakeClient is used for headless clients / bots. It does not require an incoming TCP connection and it is expected that you will simulate an actual client through Dispatch (see tera-discord-relay).

new FakeClient(connection, [keys])

connection must be an instance of Connection.

keys, if provided, must be an array of two Buffers, each of length 128. If not provided, they will be generated randomly. These keys are used to set up encryption for the connection.

Events

A FakeClient inherits from EventEmitter and emits the following events:

connect

Emitted after the initial handshake, as soon as both the client and server have shared their encryption keys.

timeout

error

These two events are forwarded from the net.Socket to the real game server.

close

Emitted when FakeClient#close() is called.