You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
388 lines
26 KiB
388 lines
26 KiB
/** |
|
* Acedia's `TcpLink` class for connecting to Avarice. |
|
* This class should be considered an internal implementation detail and not |
|
* accessed directly. |
|
* Copyright 2021 Anton Tarasenko |
|
*------------------------------------------------------------------------------ |
|
* This file is part of Acedia. |
|
* |
|
* Acedia is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU General Public License as published by |
|
* the Free Software Foundation, version 3 of the License, or |
|
* (at your option) any later version. |
|
* |
|
* Acedia is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU General Public License |
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
|
*/ |
|
class AvariceTcpStream extends TcpLink |
|
dependson(LoggerAPI); |
|
|
|
var private Global _; |
|
|
|
// Reference to the link that has spawned us, to pass on messages. |
|
var private AvariceLink ownerLink; |
|
|
|
// Information needed for connection |
|
var private string linkHost; |
|
var private int linkPort; |
|
var private IpAddr remoteAddress; |
|
|
|
// If `OpenNoSteam()` (inside `OpenAddress()`) call is made before Avarice |
|
// application started - connection will not succeed. Because of that |
|
// `AvariceTcpStream` will keep restarting its attempt to connect if it waited |
|
// for connection to go through for long enough. |
|
// This variable is set when `AvariceTcpStream` is initialized and it |
|
// defines how long we have to wait before reconnection attempt. |
|
var private float reconnectInterval; |
|
// This variable track how much time has passed since `OpenNoSteam()` call. |
|
var private float timeSpentConnecting; |
|
// For reconnections we need to remember whether we have already bound our |
|
// port, otherwise it will lead to errors in logs. |
|
var private bool portBound; |
|
|
|
// Array used to read to and write from TCP connection. |
|
// All the native methods use it, so avoid creating it locally in our methods. |
|
var private byte buffer[255]; |
|
|
|
// Used to convert our messages in a way appropriate for the network |
|
var private Utf8Encoder encoder; |
|
// Used to read and correctly interpret byte stream from Avarice |
|
var private AvariceStreamReader avariceReader; |
|
|
|
// Arbitrary value indicating that next byte sequence from us reports amount of |
|
// bytes received so far |
|
var private byte HEAD_BYTES_RECEIVED; |
|
// Arbitrary value indicating that next byte sequence from us contains |
|
// JSON message (prepended by it's length) |
|
var private byte HEAD_MESSAGE; |
|
|
|
// Byte mask to extract lowest byte from the `int`s: |
|
// 00000000 00000000 00000000 11111111 |
|
var private int byteMask; |
|
|
|
// `Text` values to be used as keys for getting information from |
|
// received messages |
|
var private Text keyS, keyT, keyP; |
|
|
|
var private LoggerAPI.Definition infoConnected; |
|
var private LoggerAPI.Definition infoDisconnected; |
|
var private LoggerAPI.Definition fatalNoLink; |
|
var private LoggerAPI.Definition fatalBadPort; |
|
var private LoggerAPI.Definition fatalCannotBindPort; |
|
var private LoggerAPI.Definition fatalCannotResolveHost; |
|
var private LoggerAPI.Definition fatalCannotConnect; |
|
var private LoggerAPI.Definition fatalInvaliddUTF8; |
|
var private LoggerAPI.Definition fatalInvalidMessage; |
|
|
|
// Starts this link, to stop it - simply destroy it |
|
public final function StartUp(AvariceLink link, float reconnectTime) |
|
{ |
|
if (link == none) |
|
{ |
|
Destroy(); |
|
return; |
|
} |
|
ownerLink = link; |
|
// Apparently `TcpLink` ignores default values for these variables, |
|
// so we set them here |
|
linkMode = MODE_Binary; |
|
receiveMode = RMODE_Manual; |
|
// This actor does not have `AcediaActor` for a parent, so manually |
|
// define `_` for convenience |
|
_ = class'Global'.static.GetInstance(); |
|
// Necessary constants |
|
keyS = _.text.FromString("s"); |
|
keyT = _.text.FromString("t"); |
|
keyP = _.text.FromString("p"); |
|
// For decoding input and encoding output |
|
encoder = Utf8Encoder(_.memory.Allocate(class'Utf8Encoder')); |
|
avariceReader = |
|
AvariceStreamReader(_.memory.Allocate(class'AvariceStreamReader')); |
|
linkHost = _.text.ToString(link.GetHost()); |
|
linkPort = link.GetPort(); |
|
reconnectInterval = reconnectTime; |
|
TryConnecting(); |
|
} |
|
|
|
private final function TryConnecting() |
|
{ |
|
if (linkPort <= 0) |
|
{ |
|
_.logger.Auto(fatalBadPort) |
|
.ArgInt(linkPort) |
|
.Arg(ownerLink.GetName()); |
|
Destroy(); |
|
return; |
|
} |
|
// `linkPort` is a port we are connecting to, which is different |
|
// from the port we have to bind to use `OpenNoSteam()` method |
|
if (!portBound && BindPort(, true) <= 0) |
|
{ |
|
_.logger.Auto(fatalCannotBindPort) |
|
.Arg(ownerLink.GetName()); |
|
Destroy(); |
|
return; |
|
} |
|
portBound = true; |
|
// Try to read `linkHost` as an IP address first, in case of failure - |
|
// try to resolve it from the host name |
|
StringToIpAddr(linkHost, remoteAddress); |
|
remoteAddress.port = linkPort; |
|
if (remoteAddress.addr == 0) { |
|
Resolve(linkHost); |
|
} |
|
else { |
|
OpenAddress(); |
|
} |
|
timeSpentConnecting = 0.0; |
|
} |
|
|
|
event Resolved(IpAddr resolvedAddress) |
|
{ |
|
remoteAddress.addr = resolvedAddress.addr; |
|
OpenAddress(); |
|
} |
|
|
|
event ResolveFailed() |
|
{ |
|
_.logger.Auto(fatalCannotResolveHost).Arg(_.text.FromString(linkHost)); |
|
Destroy(); |
|
} |
|
|
|
private final function OpenAddress() |
|
{ |
|
if (!OpenNoSteam(remoteAddress)) |
|
{ |
|
_.logger.Auto(fatalCannotConnect).Arg(ownerLink.GetName()); |
|
Destroy(); |
|
} |
|
} |
|
|
|
event Opened() |
|
{ |
|
_.logger.Auto(infoConnected).Arg(ownerLink.GetName()); |
|
if (ownerLink != none) { |
|
ownerLink.ReceiveNetworkMessage(ANM_Connected); |
|
} |
|
else { |
|
_.logger.Auto(fatalNoLink).Arg(ownerLink.GetName()); |
|
} |
|
SendMessage(_.text.FromString("{\"s\":\"echo\",\"t\":\"bounce\",\"p\":null}")); |
|
} |
|
|
|
event Closed() |
|
{ |
|
_.logger.Auto(infoDisconnected).Arg(ownerLink.GetName()); |
|
if (ownerLink != none) { |
|
ownerLink.ReceiveNetworkMessage(ANM_Connected); |
|
} |
|
else { |
|
_.logger.Auto(fatalNoLink).Arg(ownerLink.GetName()); |
|
} |
|
portBound = false; |
|
timeSpentConnecting = 0.0; |
|
} |
|
|
|
event Destroyed() |
|
{ |
|
if (_ != none) |
|
{ |
|
_.memory.Free(avariceReader); |
|
_.memory.Free(encoder); |
|
_.memory.Free(keyS); |
|
_.memory.Free(keyT); |
|
_.memory.Free(keyP); |
|
if (ownerLink != none) { |
|
ownerLink.ReceiveNetworkMessage(ANM_Death); |
|
} |
|
else { |
|
_.logger.Auto(fatalNoLink).Arg(ownerLink.GetName()); |
|
} |
|
} |
|
} |
|
|
|
event Tick(float delta) |
|
{ |
|
if (linkState == STATE_Connected) { |
|
HandleIncomingData(); |
|
} |
|
else |
|
{ |
|
timeSpentConnecting += delta; |
|
if (timeSpentConnecting >= reconnectInterval) |
|
{ |
|
Close(); |
|
TryConnecting(); |
|
timeSpentConnecting = 0.0; |
|
} |
|
} |
|
} |
|
|
|
private final function HandleIncomingData() |
|
{ |
|
local int i, totalReceivedBytes; |
|
local AvariceMessage nextMessage; |
|
local array<MutableText> receivedMessages; |
|
local int bytesRead; |
|
bytesRead = ReadBinary(255, buffer); |
|
while (bytesRead > 0) |
|
{ |
|
totalReceivedBytes += bytesRead; |
|
for (i = 0; i < bytesRead; i += 1) { |
|
avariceReader.PushByte(buffer[i]); |
|
} |
|
bytesRead = ReadBinary(255, buffer); |
|
} |
|
if (avariceReader.Failed()) |
|
{ |
|
_.logger.Auto(fatalInvaliddUTF8).Arg(ownerLink.GetName()); |
|
Destroy(); |
|
return; |
|
} |
|
// Tell Avarice how many bytes we have received, so it can send |
|
// more information |
|
if (totalReceivedBytes > 0) { |
|
SendReceived(totalReceivedBytes); |
|
} |
|
receivedMessages = avariceReader.PopMessages(); |
|
for (i = 0; i < receivedMessages.length; i += 1) |
|
{ |
|
nextMessage = MessageFromText(receivedMessages[i]); |
|
// This means received message is invalid, |
|
// which means whatever we are connected to is feeding us invalid data, |
|
// which means connection should be cut immediately |
|
if (nextMessage == none) |
|
{ |
|
_.logger.Auto(fatalInvalidMessage).Arg(ownerLink.GetName()); |
|
_.memory.Free(nextMessage); |
|
_.memory.FreeMany(receivedMessages); |
|
Destroy(); |
|
return; |
|
} |
|
ownerLink.ReceiveNetworkMessage(ANM_Message, nextMessage); |
|
_.memory.Free(nextMessage); |
|
} |
|
_.memory.FreeMany(receivedMessages); |
|
} |
|
|
|
public final function SendMessage(Text textMessage) |
|
{ |
|
local int i; |
|
local int nextByte; |
|
local ByteArrayRef message; |
|
local int messageLength; |
|
if (textMessage == none) { |
|
return; |
|
} |
|
message = encoder.Encode(textMessage); |
|
messageLength = message.GetLength(); |
|
// Signal that we are sending next message |
|
buffer[0] = HEAD_MESSAGE; |
|
// Next four bytes (with indices 1, 2, 3 and 4) must contain length of |
|
// the message's contents as a 4-byte unsigned integer in big endian. |
|
// UnrealScript does not actually have an unsigned integer type, but |
|
// even `int`'s positive value range is enough, since avarice server |
|
// will not accept message length that is even close to `MaxInt`. |
|
buffer[4] = messageLength & byteMask; |
|
messageLength -= buffer[4]; |
|
messageLength = messageLength >> 8; |
|
buffer[3] = messageLength & byteMask; |
|
messageLength -= buffer[3]; |
|
messageLength = messageLength >> 8; |
|
buffer[2] = messageLength & byteMask; |
|
messageLength -= buffer[2]; |
|
messageLength = messageLength >> 8; |
|
buffer[1] = messageLength; |
|
// Record the rest of the message in chunks of `255`, since `SendBinary()` |
|
// can only send this much at once |
|
nextByte = 5; // We have already added 5 bytes in the code above |
|
messageLength = message.GetLength(); |
|
for (i = 0; i < messageLength; i += 1) |
|
{ |
|
buffer[nextByte] = message.GetItem(i); |
|
nextByte += 1; |
|
if (nextByte >= 255) |
|
{ |
|
nextByte = 0; |
|
SendBinary(255, buffer); |
|
} |
|
} |
|
// Cycle above only sent full chunks of `255`, so send the remainder now |
|
if (nextByte > 0) { |
|
SendBinary(nextByte, buffer); |
|
} |
|
message.FreeSelf(); |
|
} |
|
|
|
private final function SendReceived(int received) |
|
{ |
|
// Signal that we are sending amount of bytes received this tick |
|
buffer[0] = HEAD_BYTES_RECEIVED; |
|
// Next four bytes (with indices 1 and 2) must contain amount of bytes |
|
// received as a 2-byte unsigned integer in big endian |
|
buffer[2] = received & byteMask; |
|
received -= buffer[2]; |
|
received = received >> 8; |
|
buffer[1] = received; |
|
SendBinary(3, buffer); |
|
} |
|
|
|
private final function AvariceMessage MessageFromText(Text message) |
|
{ |
|
local Parser parser; |
|
local AvariceMessage result; |
|
local AssociativeArray parsedMessage; |
|
local AcediaObject item; |
|
if (message == none) { |
|
return none; |
|
} |
|
parser = _.text.Parse(message); |
|
parsedMessage = _.json.ParseObjectWith(parser); |
|
parser.FreeSelf(); |
|
if (parsedMessage == none) { |
|
return none; |
|
} |
|
result = AvariceMessage(_.memory.Allocate(class'AvariceMessage')); |
|
item = parsedMessage.TakeItem(keyS, true); |
|
if (item == none || item.class != class'Text') |
|
{ |
|
_.memory.Free(item); |
|
_.memory.Free(parsedMessage); |
|
_.memory.Free(result); |
|
return none; |
|
} |
|
result.service = Text(item); |
|
item = parsedMessage.TakeItem(keyT, true); |
|
if (item == none || item.class != class'Text') |
|
{ |
|
_.memory.Free(item); |
|
_.memory.Free(parsedMessage); |
|
_.memory.Free(result); |
|
return none; |
|
} |
|
result.type = Text(item); |
|
result.parameters = parsedMessage.TakeItem(keyP, true); |
|
_.memory.Free(parsedMessage); |
|
return result; |
|
} |
|
|
|
defaultproperties |
|
{ |
|
HEAD_BYTES_RECEIVED = 85 |
|
HEAD_MESSAGE = 42 |
|
byteMask = 255 // Only lowest 8 bits are `1` |
|
infoConnected = (l=LOG_Info,m="Avarice link \"%1\" connected") |
|
infoDisconnected = (l=LOG_Info,m="Avarice link \"%1\" disconnected") |
|
fatalNoLink = (l=LOG_Fatal,m="Unexpected internal `none` value for Avarice link \"%1\"") |
|
fatalBadPort = (l=LOG_Fatal,m="Bad port \"%1\" specified for Avarice link \"%2\"") |
|
fatalCannotBindPort = (l=LOG_Fatal,m="Cannot bind port for Avarice link \"%1\"") |
|
fatalCannotResolveHost = (l=LOG_Fatal,m="Cannot resolve host \"%1\" for Avarice link \"%2\"") |
|
fatalCannotConnect = (l=LOG_Fatal,m="Connection for Avarice link \"%1\" was rejected") |
|
fatalInvaliddUTF8 = (l=LOG_Fatal,m="Avarice link \"%1\" has received invalid UTF8, aborting connection") |
|
fatalInvalidMessage = (l=LOG_Fatal,m="Avarice link \"%1\" has received invalid message, aborting connection") |
|
} |