diff --git a/building/libs.xml b/building/libs.xml index 139cbaf8a..ee816439a 100644 --- a/building/libs.xml +++ b/building/libs.xml @@ -18,6 +18,7 @@ + diff --git a/project.xml b/project.xml index 3d6399a14..8d5da9ca8 100644 --- a/project.xml +++ b/project.xml @@ -127,6 +127,8 @@ + + diff --git a/source/funkin/backend/system/net/FunkinPacket.hx b/source/funkin/backend/system/net/FunkinPacket.hx new file mode 100644 index 000000000..6c1c9c0d4 --- /dev/null +++ b/source/funkin/backend/system/net/FunkinPacket.hx @@ -0,0 +1,76 @@ +package funkin.backend.system.net; + +import flixel.util.typeLimit.OneOfTwo; + +import haxe.io.Bytes; +import haxe.io.BytesOutput; + +class FunkinPacket { + public var status:Int = -1; + public var head:String; + public var fields:Map = new Map(); + public var body:OneOfTwo; + + public function new(_head:String, ?_body:OneOfTwo = "", ?_status:Int = -1) { + this.head = _head.trim(); + this.body = (_body is String) ? _body.trim() : _body; + this.status = _status; + } + + public function set(name:String, value:String):FunkinPacket { fields.set(name, value); return this; } + inline public function get(name:String):String { return fields.get(name); } + inline public function exists(name:String):Bool { return fields.exists(name); } + inline public function remove(name:String):Bool { return fields.remove(name); } + inline public function keys():Iterator { return fields.keys(); } + + public function toString(?includeBody:Bool = true):String { + var str:String = ''; + if (head.length > 0) str += '$head\r\n'; + for (key in keys()) str += '$key: ${get(key)}\r\n'; + if (!includeBody) return str; + if (body is String) str += body; + else str += (cast body : Bytes).toString(); + return str; + } + public function toBytes():Bytes { + var bytes:BytesOutput = new BytesOutput(); + bytes.writeString(toString(false)); // Absolute Cinema, thanks AbstractAndrew for the Revolutionary Idea 🔥🔥 + if (body is String) bytes.writeString(body); + else if (body is Bytes) bytes.write(body); + return bytes.getBytes(); + } + + public static function fromBytes(bytes:Bytes):Null { + var status:Int = -1; + + var header_length:Int = -1; + var header:String = ""; + + var body_is_string:Bool = false; + var body_length:Int = -1; + var body:OneOfTwo = null; + + try { + var offset:Int = 0; + status = bytes.getInt32(0); offset += 4; + header_length = bytes.getInt32(offset); offset += 4; + header = bytes.getString(offset, header_length); offset += header_length; + body_is_string = (bytes.get(offset) == 1); offset += 1; + body_length = bytes.getInt32(offset); offset += 4; + if (body_is_string) body = bytes.getString(offset, body_length); + else body = bytes.sub(offset, body_length); + } catch(e) { + FlxG.log.error('FunkinPacket.fromBytes() failed to parse packet: $e'); + return null; + } + var packet:FunkinPacket = new FunkinPacket(null, body, status); + for (line in header.split("\r\n")) { + var data = line.split(": "); + if (data.length < 2) continue; + var key:String = data.shift().trim(); + var value:String = data.shift().trim(); + packet.set(key, value); + } + return packet; + } +} \ No newline at end of file diff --git a/source/funkin/backend/system/net/FunkinSocket.hx b/source/funkin/backend/system/net/FunkinSocket.hx new file mode 100644 index 000000000..ef77fd360 --- /dev/null +++ b/source/funkin/backend/system/net/FunkinSocket.hx @@ -0,0 +1,129 @@ +package funkin.backend.system.net; + +#if sys +import sys.net.Host; +import sys.net.Socket as SysSocket; +import haxe.io.Bytes; + +@:keep +class FunkinSocket implements IFlxDestroyable { + public var socket:SysSocket = new SysSocket(); + + public var metrics:Metrics = new Metrics(); + + public var FAST_SEND(default, set):Bool = true; + private function set_FAST_SEND(value:Bool):Bool { + FAST_SEND = value; + socket.setFastSend(value); + return value; + } + public var BLOCKING(default, set):Bool = false; + private function set_BLOCKING(value:Bool):Bool { + BLOCKING = value; + socket.setBlocking(value); + return value; + } + + public var host:Host; + public var port:Int; + + public function new(?_host:String = "127.0.0.1", ?_port:Int = 5000) { + FAST_SEND = true; + BLOCKING = false; + this.host = new Host(_host); + this.port = _port; + } + + // Reading Area + public function readAll():Null { + try { + var bytes = this.socket.input.readAll(); + if (bytes == null) return null; + metrics.updateBytesReceived(bytes.length); + return bytes; + } catch(e) { } + return null; + } + public function readLine():Null { + try { + var bytes = this.socket.input.readLine(); + if (bytes == null) return null; + metrics.updateBytesReceived(bytes.length); + return bytes; + } catch(e) { } + return null; + } + public function read(nBytes:Int):Null { + try { + var bytes = this.socket.input.read(nBytes); + if (bytes == null) return null; + metrics.updateBytesReceived(bytes.length); + return bytes; + } catch(e) { } + return null; + } + public function readBytes(bytes:Bytes):Int { + try { + var length = this.socket.input.readBytes(bytes, 0, bytes.length); + metrics.updateBytesReceived(length); + return length; + } catch(e) { } + return 0; + } + + // Writing Area + public function prepare(nbytes:Int):Void { socket.output.prepare(nbytes); } + public function write(bytes:Bytes):Bool { + try { + this.socket.output.write(bytes); + metrics.updateBytesSent(bytes.length); + return true; + } catch (e) { } + return false; + } + public function writeString(str:String):Bool { + try { + this.socket.output.writeString(str); + metrics.updateBytesSent(Bytes.ofString(str).length); + return true; + } catch(e) { } + return false; + } + + public function bind(?expectingConnections:Int = 1):FunkinSocket { + Logs.traceColored([ + Logs.logText('[FunkinSocket] ', BLUE), + Logs.logText('Binding to ', NONE), Logs.logText(host.toString(), YELLOW), Logs.logText(':', NONE), Logs.logText(Std.string(port), CYAN), + ]); + socket.bind(host, port); + socket.listen(expectingConnections); + return this; + } + + public function connect():FunkinSocket { + Logs.traceColored([ + Logs.logText('[FunkinSocket] ', BLUE), + Logs.logText('Connecting to ', NONE), Logs.logText(host.toString(), YELLOW), Logs.logText(':', NONE), Logs.logText(Std.string(port), CYAN), + ]); + socket.connect(host, port); + return this; + } + + public function close() { + try { + if (socket != null) socket.close(); + Logs.traceColored([ + Logs.logText('[FunkinSocket] ', BLUE), + Logs.logText('Closing socket from ', NONE), Logs.logText(host.toString(), YELLOW), Logs.logText(':', NONE), Logs.logText(Std.string(port), CYAN), + ]); + } catch(e) { + Logs.traceColored([ + Logs.logText('[FunkinSocket] ', BLUE), + Logs.logText('Failed to close socket: ${e}', NONE), + ]); + } + } + + public function destroy() { close(); } +} +#end \ No newline at end of file diff --git a/source/funkin/backend/system/net/FunkinWebSocket.hx b/source/funkin/backend/system/net/FunkinWebSocket.hx new file mode 100644 index 000000000..4214bcb56 --- /dev/null +++ b/source/funkin/backend/system/net/FunkinWebSocket.hx @@ -0,0 +1,180 @@ +package funkin.backend.system.net; + +import flixel.util.typeLimit.OneOfThree; + +import haxe.io.Bytes; + +import flixel.util.FlxSignal.FlxTypedSignal; +import hx.ws.Log as LogWs; +import hx.ws.WebSocket; +import hx.ws.Types.MessageType; + +/** +* This is a wrapper for hxWebSockets. Used in-tangem with `FunkinPacket` and `Metrics`. +* By default, it assums the Connections is to a [CodenameEngine Template Server](https://github.com/ItsLJcool/CodenameEngine-Online-Template). +* You can override how `haxe.io.Bytes` is decoded by setting `AUTO_DECODE_PACKETS`. By default it will attempt to deserialize the packet into a `FunkinPacket`. +* It also has `Metrics` which keeps track of the amount of bytes sent and received. +**/ +class FunkinWebSocket implements IFlxDestroyable { + /** + * This interacts with the hxWebSockets logging system, probably the best way to get the debug info. + * Although, it's not in the format of CodenameEngine's logs so it might look weird. + **/ + private static var LOG_INFO(default, set):Bool = false; + private static function set_LOG_INFO(value:Bool):Bool { + LOG_INFO = value; + if (LOG_INFO) LogWs.mask |= LogWs.INFO; + else LogWs.mask &= ~LogWs.INFO; + return value; + } + /** + * Ditto to LOG_INFO + **/ + private static var LOG_DEBUG(default, set):Bool = false; + private static function set_LOG_DEBUG(value:Bool):Bool { + LOG_DEBUG = value; + if (LOG_DEBUG) LogWs.mask |= LogWs.DEBUG; + else LogWs.mask &= ~LogWs.DEBUG; + return value; + } + /** + * Ditto to LOG_INFO + **/ + private static var LOG_DATA(default, set):Bool = false; + private static function set_LOG_DATA(value:Bool):Bool { + LOG_DATA = value; + if (LOG_DATA) LogWs.mask |= LogWs.DATA; + else LogWs.mask &= ~LogWs.DATA; + return value; + } + + @:dox(hide) private var _ws:WebSocket; + + /** + * This keeps track of the amount of bytes sent and received. + * You can set the logging state directly in the class. + **/ + public var metrics:Metrics = new Metrics(); + + /** + * This signal is only called once when the connection is opened. + **/ + public var onOpen:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + /** + * This signal is called every time a message is received. + * It can be one of three types: String, Bytes, or FunkinPacket. + * If you have AUTO_DECODE_PACKETS set to true, It will attempt to deserialize the packet into a FunkinPacket. + * If it fails to deserialize or AUTO_DECODE_PACKETS is false, it will just return the Bytes directly. + **/ + public var onMessage:FlxTypedSignal->Void> = new FlxTypedSignal->Void>(); // cursed 😭😭 + /** + * This signal is only called once when the connection is closed. + **/ + public var onClose:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + /** + * This signal is only called when an error occurs. Useful for debugging and letting the user know something has gone wrong. + **/ + public var onError:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + + /** + * The URL to connect to, including the protocol (ws:// or wss://). Currently wss:// (SSH) is not supported. + **/ + public var url:String; + + /** + * This just allows you to override or add custom headers when the handshake happens, as this is the first and last time we use proper HTTP Headers. + **/ + public var handshakeHeaders(get, null):Map; + public function get_handshakeHeaders():Map { return this._ws.additionalHeaders; } + + /** + * Since not all servers are going to be the Custom CodenameEngine Template Server, this allows you to receive the packet as raw Bytes, if you want to decode it yourself. + * Not all incomming data will be bytes, since Strings are just... strings, there is no reason to have special handling for them. + **/ + public var AUTO_DECODE_PACKETS:Bool = true; + + /** + * This is only called if the `Metrics` failed to update the bytes sent or received. + * So you can handle and update the data yourself. + * If Bool is true, the data was being sent. Otherwise it was being received. + **/ + private var updateMetricCustom:(Metrics, Bool, Dynamic)->Void = null; + + /** + * @param _url The URL to connect to, including the protocol (ws:// or wss://). Currently wss:// (SSH) is not supported. + **/ + public function new(_url:String) { + this.url = _url; + + this._ws = new WebSocket(this.url, false); + this._ws.onopen = () -> onOpen.dispatch(); + this._ws.onmessage = (message:MessageType) -> { + var data:OneOfThree = ""; + switch(message) { + case StrMessage(content): + data = content; + metrics.updateBytesReceived(Bytes.ofString(content).length); + case BytesMessage(buffer): + metrics.updateBytesReceived(buffer.length); + data = buffer.readAllAvailableBytes(); + if (!AUTO_DECODE_PACKETS) return onMessage.dispatch(data); + var packet:FunkinPacket = FunkinPacket.fromBytes(data); + if (packet == null) return onMessage.dispatch(data); + data = packet; + } + onMessage.dispatch(data); + }; + this._ws.onclose = () -> onClose.dispatch(); + this._ws.onerror = (error) -> onError.dispatch(error); + } + + /** + * Opens the WebSocket connection. + **/ + public function open():FunkinWebSocket { + Logs.traceColored([ + Logs.logText('[FunkinWebSocket] ', CYAN), + Logs.logText('Opening WebSocket to ', NONE), Logs.logText(url, YELLOW), + ]); + this._ws.open(); + return this; + } + + /** + * Sends data to the server. + * @param data The data to send. + **/ + public function send(data:Dynamic):Bool { + try { + this._ws.send(data); + if (data is String) metrics.updateBytesSent(Bytes.ofString(data).length); + else if (data is Bytes) metrics.updateBytesSent(data.length); + else if (data is FunkinPacket) metrics.updateBytesSent(data.toBytes().length); + else if (metrics.IS_LOGGING && updateMetricCustom != null) updateMetricCustom(metrics, true, data); + return true; + } catch(e) { + Logs.traceColored([ + Logs.logText('[FunkinWebSocket] ', CYAN), + Logs.logText('Failed to send data: ${e}', NONE), + ]); + } + return false; + } + + /** + * Closes the WebSocket connection. + * Once you close the connection, you cannot reopen it, you must create a new instance. + **/ + public function close():Void { + Logs.traceColored([ + Logs.logText('[FunkinWebSocket] ', CYAN), + Logs.logText('Closing WebSocket from ', NONE), Logs.logText(url, YELLOW), + ]); + this._ws.close(); + } + + /** + * Basically the same as close(), but if a class is handling it and expects it to be IFlxDestroyable compatable, it will call this. + **/ + public function destroy():Void { close(); } +} \ No newline at end of file diff --git a/source/funkin/backend/system/net/Metrics.hx b/source/funkin/backend/system/net/Metrics.hx new file mode 100644 index 000000000..0e44aa76d --- /dev/null +++ b/source/funkin/backend/system/net/Metrics.hx @@ -0,0 +1,29 @@ +package funkin.backend.system.net; + +class Metrics { + public var bytesSent:Int = 0; + public var bytesReceived:Int = 0; + + public var packetsSent:Int = 0; + public var packetsReceived:Int = 0; + + public var IS_LOGGING:Bool = true; + + public function new() { } + + public function updateBytesSent(amount:Int) { + if (!IS_LOGGING) return; + bytesSent += amount; + packetsSent++; + } + + public function updateBytesReceived(amount:Int) { + if (!IS_LOGGING) return; + bytesReceived += amount; + packetsReceived++; + } + + public function toString():String { + return '(Metrics) $bytesSent bytes sent | $bytesReceived bytes received | $packetsSent packets sent | $packetsReceived packets received'; + } +} \ No newline at end of file diff --git a/source/funkin/backend/system/net/Socket.hx b/source/funkin/backend/system/net/Socket.hx deleted file mode 100644 index 19ebf9a11..000000000 --- a/source/funkin/backend/system/net/Socket.hx +++ /dev/null @@ -1,65 +0,0 @@ -package funkin.backend.system.net; - -#if sys -import sys.net.Host; -import sys.net.Socket as SysSocket; - -@:keep -class Socket implements IFlxDestroyable { - public var socket:SysSocket; - - public function new(?socket:SysSocket) { - this.socket = socket; - if (this.socket == null) - this.socket = new SysSocket(); - this.socket.setFastSend(true); - this.socket.setBlocking(false); - } - - public function read():String { - try { - return this.socket.input.readUntil('\n'.code).replace("\\n", "\n"); - } catch(e) { - - } - return null; - } - - public function write(str:String):Bool { - try { - this.socket.output.writeString(str.replace("\n", "\\n")); - return true; - } catch(e) { - - } - return false; - } - - public function host(host:Host, port:Int, nbConnections:Int = 1) { - socket.bind(host, port); - socket.listen(nbConnections); - socket.setFastSend(true); - } - - public function hostAndWait(h:Host, port:Int) { - host(h, port); - return acceptConnection(); - } - - public function acceptConnection():Socket { - socket.setBlocking(true); - var accept = new Socket(socket.accept()); - socket.setBlocking(false); - return accept; - } - - public function connect(host:Host, port:Int) { - socket.connect(host, port); - } - - public function destroy() { - if (socket != null) - socket.close(); - } -} -#end \ No newline at end of file