/** * 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 . */ 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) .ArgInt(linkPort) .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()); } } 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) { TryConnecting(); timeSpentConnecting = 0.0; } } } private final function HandleIncomingData() { local int i, totalReceivedBytes; local AvariceMessage nextMessage; local array 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") }