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.
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.
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.
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.
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 higherorder
. 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, andnull
disables the filter. All of them are optional.-
fake
: Filters "fake" packets—those generated throughDispatch
. Default:false
(only allow real packets). Iftrue
, matches only packets generated throughDispatch
. 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). Iftrue
, matches only packets sent to the client. Iffalse
, matches only packets sent to the server. -
modified
: Filters packets if they have been altered by a previous hook. Default:null
(ignore modification status). Iftrue
, matches only packets modified by previous hooks. Iffalse
, matches only unmodified packets. -
silenced
: Filters packets if they have been silenced by a previous hook. Default:false
(only allow unsilenced packets). Iftrue
, matches only packets that have had a previous hookreturn false
to prevent the packet from being sent. Any hook that receives a silenced packet may un-silence the packet by returningtrue
.
-
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 (seetera-data
).fake
:true
if this packet was generated through the proxy,false
otherwise.- Return value is
true
ifevent
is modified, orfalse
to stop and silence the message. Other return values are ignored. If you changeevent
but do not returntrue
, the changes will not be saved.
- For a
raw
hook,code
: The opcode of the message as an integer.data
: TheBuffer
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, orfalse
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 returntrue
.
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.
Removes a hook. It will no longer be called. Pass in the result of a call to Dispatch#hook()
.
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.
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.
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()
.
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()
.
Passes the (usually parsed) data all registered hooks.
Return value is the raw data buffer, or false
if silenced by any hook.
Unloads all modules and removes all hooks.
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.
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}`);
});
This class handles encryption and packet buffering for a socket to a real game server.
Sets up a connection handler. This also initializes an instance of Dispatch
unique to this Connection
.
An intsance of Dispatch
.
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.
Ends the connection and calls Dispatch#reset()
.
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.
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.
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
).
connection
must be an instance of Connection
.
keys
, if provided, must be an array of two Buffer
s, each of length 128. If not provided, they will be generated randomly. These keys are used to set up encryption for the connection.
A FakeClient
inherits from EventEmitter
and emits the following events:
Emitted after the initial handshake, as soon as both the client and server have shared their encryption keys.
These two events are forwarded from the net.Socket
to the real game server.
Emitted when FakeClient#close()
is called.