diff --git a/phpstan.neon.dist b/phpstan.neon.dist index eed79df..bf55dcb 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,6 +6,7 @@ parameters: level: 9 paths: - src + - tools reportUnmatchedIgnoredErrors: false #enabling this makes build results too volatile when phpstan bugs get fixed ignoreErrors: - diff --git a/src/RakLib.php b/src/RakLib.php index 6db00f1..fce15cb 100644 --- a/src/RakLib.php +++ b/src/RakLib.php @@ -23,9 +23,6 @@ abstract class RakLib{ */ public const DEFAULT_PROTOCOL_VERSION = 6; - /** - * Regular RakNet uses 10 by default. MCPE uses 20. Configure this value as appropriate. - * @var int - */ - public static $SYSTEM_ADDRESS_COUNT = 20; + /** Regular RakNet uses 10 by default. MCPE uses 20. Configure this value as appropriate. */ + public static int $SYSTEM_ADDRESS_COUNT = 20; } diff --git a/src/client/ClientSocket.php b/src/client/ClientSocket.php new file mode 100644 index 0000000..73a2b6d --- /dev/null +++ b/src/client/ClientSocket.php @@ -0,0 +1,75 @@ + + * + * RakLib is not affiliated with Jenkins Software LLC nor RakNet. + * + * RakLib 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, either version 3 of the License, or + * (at your option) any later version. + */ + +declare(strict_types=1); + +namespace raklib\client; + +use raklib\generic\Socket; +use raklib\generic\SocketException; +use raklib\utils\InternetAddress; +use function socket_connect; +use function socket_last_error; +use function socket_recv; +use function socket_send; +use function socket_strerror; +use function strlen; +use function trim; + +class ClientSocket extends Socket{ + + public function __construct( + private InternetAddress $connectAddress + ){ + parent::__construct($this->connectAddress->getVersion() === 6); + + if(!@socket_connect($this->socket, $this->connectAddress->getIp(), $this->connectAddress->getPort())){ + $error = socket_last_error($this->socket); + throw new SocketException("Failed to connect to " . $this->connectAddress . ": " . trim(socket_strerror($error)), $error); + } + //TODO: is an 8 MB buffer really appropriate for a client?? + $this->setSendBuffer(1024 * 1024 * 8)->setRecvBuffer(1024 * 1024 * 8); + } + + public function getConnectAddress() : InternetAddress{ + return $this->connectAddress; + } + + /** + * @throws SocketException + */ + public function readPacket() : ?string{ + $buffer = ""; + if(@socket_recv($this->socket, $buffer, 65535, 0) === false){ + $errno = socket_last_error($this->socket); + if($errno === SOCKET_EWOULDBLOCK){ + return null; + } + throw new SocketException("Failed to recv (errno $errno): " . trim(socket_strerror($errno)), $errno); + } + return $buffer; + } + + /** + * @throws SocketException + */ + public function writePacket(string $buffer) : int{ + $result = @socket_send($this->socket, $buffer, strlen($buffer), 0); + if($result === false){ + $errno = socket_last_error($this->socket); + throw new SocketException("Failed to send packet (errno $errno): " . trim(socket_strerror($errno)), $errno); + } + return $result; + } +} diff --git a/src/generic/DisconnectReason.php b/src/generic/DisconnectReason.php new file mode 100644 index 0000000..d03c3b5 --- /dev/null +++ b/src/generic/DisconnectReason.php @@ -0,0 +1,36 @@ + + * + * RakLib is not affiliated with Jenkins Software LLC nor RakNet. + * + * RakLib 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, either version 3 of the License, or + * (at your option) any later version. + */ + +declare(strict_types=1); + +namespace raklib\generic; + +final class DisconnectReason{ + public const CLIENT_DISCONNECT = 0; + public const SERVER_DISCONNECT = 1; + public const PEER_TIMEOUT = 2; + public const CLIENT_RECONNECT = 3; + public const SERVER_SHUTDOWN = 4; //TODO: do we really need a separate reason for this in addition to SERVER_DISCONNECT? + + public static function toString(int $reason) : string{ + return match($reason){ + self::CLIENT_DISCONNECT => "client disconnect", + self::SERVER_DISCONNECT => "server disconnect", + self::PEER_TIMEOUT => "timeout", + self::CLIENT_RECONNECT => "new session established on same address and port", + self::SERVER_SHUTDOWN => "server shutdown", + default => "Unknown reason $reason" + }; + } +} diff --git a/src/generic/ReceiveReliabilityLayer.php b/src/generic/ReceiveReliabilityLayer.php index 90a306d..415f5e2 100644 --- a/src/generic/ReceiveReliabilityLayer.php +++ b/src/generic/ReceiveReliabilityLayer.php @@ -28,71 +28,44 @@ final class ReceiveReliabilityLayer{ - /** @var int */ - public static $WINDOW_SIZE = 2048; + public static int $WINDOW_SIZE = 2048; - /** @var \Logger */ - private $logger; - - /** - * @var \Closure - * @phpstan-var \Closure(EncapsulatedPacket) : void - */ - private $onRecv; - - /** - * @var \Closure - * @phpstan-var \Closure(AcknowledgePacket) : void - */ - private $sendPacket; - - /** @var int */ - private $windowStart; - /** @var int */ - private $windowEnd; - /** @var int */ - private $highestSeqNumber = -1; + private int $windowStart; + private int $windowEnd; + private int $highestSeqNumber = -1; /** @var int[] */ - private $ACKQueue = []; + private array $ACKQueue = []; /** @var int[] */ - private $NACKQueue = []; + private array $NACKQueue = []; - /** @var int */ - private $reliableWindowStart; - /** @var int */ - private $reliableWindowEnd; + private int $reliableWindowStart; + private int $reliableWindowEnd; /** @var bool[] */ - private $reliableWindow = []; + private array $reliableWindow = []; /** @var int[] */ - private $receiveOrderedIndex; + private array $receiveOrderedIndex; /** @var int[] */ - private $receiveSequencedHighestIndex; + private array $receiveSequencedHighestIndex; /** @var EncapsulatedPacket[][] */ - private $receiveOrderedPackets; + private array $receiveOrderedPackets; /** @var (EncapsulatedPacket|null)[][] */ - private $splitPackets = []; - - /** - * @var int - * @phpstan-var positive-int - */ - private $maxSplitPacketPartCount; - /** @var int */ - private $maxConcurrentSplitPackets; + private array $splitPackets = []; /** * @phpstan-param positive-int $maxSplitPacketPartCount * @phpstan-param \Closure(EncapsulatedPacket) : void $onRecv * @phpstan-param \Closure(AcknowledgePacket) : void $sendPacket */ - public function __construct(\Logger $logger, \Closure $onRecv, \Closure $sendPacket, int $maxSplitPacketPartCount = PHP_INT_MAX, int $maxConcurrentSplitPackets = PHP_INT_MAX){ - $this->logger = $logger; - $this->onRecv = $onRecv; - $this->sendPacket = $sendPacket; - + public function __construct( + private \Logger $logger, + private \Closure $onRecv, + private \Closure $sendPacket, + private int $maxSplitPacketPartCount = PHP_INT_MAX, + private int $maxConcurrentSplitPackets = PHP_INT_MAX + ){ $this->windowStart = 0; $this->windowEnd = self::$WINDOW_SIZE; @@ -103,9 +76,6 @@ public function __construct(\Logger $logger, \Closure $onRecv, \Closure $sendPac $this->receiveSequencedHighestIndex = array_fill(0, PacketReliability::MAX_ORDER_CHANNELS, 0); $this->receiveOrderedPackets = array_fill(0, PacketReliability::MAX_ORDER_CHANNELS, []); - - $this->maxSplitPacketPartCount = $maxSplitPacketPartCount; - $this->maxConcurrentSplitPackets = $maxConcurrentSplitPackets; } private function handleEncapsulatedPacketRoute(EncapsulatedPacket $pk) : void{ diff --git a/src/generic/ReliableCacheEntry.php b/src/generic/ReliableCacheEntry.php index d674972..1d2c1e3 100644 --- a/src/generic/ReliableCacheEntry.php +++ b/src/generic/ReliableCacheEntry.php @@ -21,16 +21,14 @@ final class ReliableCacheEntry{ - /** @var EncapsulatedPacket[] */ - private $packets; - /** @var float */ - private $timestamp; + private float $timestamp; /** * @param EncapsulatedPacket[] $packets */ - public function __construct(array $packets){ - $this->packets = $packets; + public function __construct( + private array $packets + ){ $this->timestamp = microtime(true); } diff --git a/src/generic/SendReliabilityLayer.php b/src/generic/SendReliabilityLayer.php index 7e9c930..c8909d6 100644 --- a/src/generic/SendReliabilityLayer.php +++ b/src/generic/SendReliabilityLayer.php @@ -22,7 +22,6 @@ use raklib\protocol\NACK; use raklib\protocol\PacketReliability; use raklib\protocol\SplitPacketInfo; -use raklib\server\Session; use function array_fill; use function count; use function str_split; @@ -30,60 +29,39 @@ use function time; final class SendReliabilityLayer{ - - /** - * @var \Closure - * @phpstan-var \Closure(Datagram) : void - */ - private $sendDatagramCallback; - /** - * @var \Closure - * @phpstan-var \Closure(int) : void - */ - private $onACK; - - /** - * @var int - * @phpstan-var int - */ - private $mtuSize; - /** @var EncapsulatedPacket[] */ - private $sendQueue = []; + private array $sendQueue = []; - /** @var int */ - private $splitID = 0; + private int $splitID = 0; - /** @var int */ - private $sendSeqNumber = 0; + private int $sendSeqNumber = 0; - /** @var int */ - private $messageIndex = 0; + private int $messageIndex = 0; /** @var int[] */ - private $sendOrderedIndex; + private array $sendOrderedIndex; /** @var int[] */ - private $sendSequencedIndex; + private array $sendSequencedIndex; /** @var ReliableCacheEntry[] */ - private $resendQueue = []; + private array $resendQueue = []; /** @var ReliableCacheEntry[] */ - private $reliableCache = []; + private array $reliableCache = []; /** @var int[][] */ - private $needACK = []; + private array $needACK = []; /** * @phpstan-param int $mtuSize - * @phpstan-param \Closure(Datagram) : void $sendDatagram + * @phpstan-param \Closure(Datagram) : void $sendDatagramCallback * @phpstan-param \Closure(int) : void $onACK */ - public function __construct(int $mtuSize, \Closure $sendDatagram, \Closure $onACK){ - $this->mtuSize = $mtuSize; - $this->sendDatagramCallback = $sendDatagram; - $this->onACK = $onACK; - + public function __construct( + private int $mtuSize, + private \Closure $sendDatagramCallback, + private \Closure $onACK + ){ $this->sendOrderedIndex = array_fill(0, PacketReliability::MAX_ORDER_CHANNELS, 0); $this->sendSequencedIndex = array_fill(0, PacketReliability::MAX_ORDER_CHANNELS, 0); } diff --git a/src/server/Session.php b/src/generic/Session.php similarity index 65% rename from src/server/Session.php rename to src/generic/Session.php index 8fe927b..fc92e8e 100644 --- a/src/server/Session.php +++ b/src/generic/Session.php @@ -14,31 +14,28 @@ declare(strict_types=1); -namespace raklib\server; +namespace raklib\generic; -use raklib\generic\ReceiveReliabilityLayer; -use raklib\generic\SendReliabilityLayer; use raklib\protocol\ACK; use raklib\protocol\AcknowledgePacket; use raklib\protocol\ConnectedPacket; use raklib\protocol\ConnectedPing; use raklib\protocol\ConnectedPong; -use raklib\protocol\ConnectionRequest; -use raklib\protocol\ConnectionRequestAccepted; use raklib\protocol\Datagram; use raklib\protocol\DisconnectionNotification; use raklib\protocol\EncapsulatedPacket; use raklib\protocol\MessageIdentifiers; use raklib\protocol\NACK; -use raklib\protocol\NewIncomingConnection; use raklib\protocol\Packet; use raklib\protocol\PacketReliability; use raklib\protocol\PacketSerializer; use raklib\utils\InternetAddress; +use function hrtime; +use function intdiv; use function microtime; use function ord; -class Session{ +abstract class Session{ public const MAX_SPLIT_PART_COUNT = 128; public const MAX_CONCURRENT_SPLIT_COUNT = 4; @@ -50,59 +47,37 @@ class Session{ public const MIN_MTU_SIZE = 400; - /** @var Server */ - private $server; + private \Logger $logger; - /** @var \Logger */ - private $logger; + protected InternetAddress $address; - /** @var InternetAddress */ - private $address; + protected int $state = self::STATE_CONNECTING; - /** @var int */ - private $state = self::STATE_CONNECTING; + private int $id; - /** @var int */ - private $id; + private float $lastUpdate; + private float $disconnectionTime = 0; - /** @var float */ - private $lastUpdate; - /** @var float */ - private $disconnectionTime = 0; + private bool $isActive = false; - /** @var bool */ - private $isTemporal = true; + private float $lastPingTime = -1; - /** @var bool */ - private $isActive = false; + private int $lastPingMeasure = 1; - /** @var float */ - private $lastPingTime = -1; - /** @var int */ - private $lastPingMeasure = 1; + private ReceiveReliabilityLayer $recvLayer; - /** @var int */ - private $internalId; + private SendReliabilityLayer $sendLayer; - /** @var ReceiveReliabilityLayer */ - private $recvLayer; - - /** @var SendReliabilityLayer */ - private $sendLayer; - - public function __construct(Server $server, \Logger $logger, InternetAddress $address, int $clientId, int $mtuSize, int $internalId){ + public function __construct(\Logger $logger, InternetAddress $address, int $clientId, int $mtuSize){ if($mtuSize < self::MIN_MTU_SIZE){ throw new \InvalidArgumentException("MTU size must be at least " . self::MIN_MTU_SIZE . ", got $mtuSize"); } - $this->server = $server; $this->logger = new \PrefixedLogger($logger, "Session: " . $address->toString()); $this->address = $address; $this->id = $clientId; $this->lastUpdate = microtime(true); - $this->internalId = $internalId; - $this->recvLayer = new ReceiveReliabilityLayer( $this->logger, function(EncapsulatedPacket $pk) : void{ @@ -120,16 +95,55 @@ function(Datagram $datagram) : void{ $this->sendPacket($datagram); }, function(int $identifierACK) : void{ - $this->server->getEventListener()->onPacketAck($this->internalId, $identifierACK); + $this->onPacketAck($identifierACK); } ); } /** - * Returns an ID used to identify this session across threads. + * Sends a packet in the appropriate way for the session type. + */ + abstract protected function sendPacket(Packet $packet) : void; + + /** + * Called when a packet for which an ACK was requested is ACKed. + */ + abstract protected function onPacketAck(int $identifierACK) : void; + + /** + * Called when the session is terminated for any reason. + * + * @param int $reason one of the DisconnectReason::* constants + * @phpstan-param DisconnectReason::* $reason + * + * @see DisconnectReason */ - public function getInternalId() : int{ - return $this->internalId; + abstract protected function onDisconnect(int $reason) : void; + + /** + * Called when a packet is received while the session is in the "connecting" state. This should only handle RakNet + * connection packets. Any other packets should be ignored. + */ + abstract protected function handleRakNetConnectionPacket(string $packet) : void; + + /** + * Called when a user packet (ID >= ID_USER_PACKET_ENUM) is received from the remote peer. + * + * @see MessageIdentifiers::ID_USER_PACKET_ENUM + */ + abstract protected function onPacketReceive(string $packet) : void; + + /** + * Called when a new ping measurement is recorded. + */ + abstract protected function onPingMeasure(int $pingMS) : void; + + /** + * Returns a monotonically increasing timestamp. It does not need to match UNIX time. + * This is used to calculate ping. + */ + protected function getRakNetTimeMS() : int{ + return intdiv(hrtime(true), 1_000_000); } public function getAddress() : InternetAddress{ @@ -144,8 +158,8 @@ public function getState() : int{ return $this->state; } - public function isTemporal() : bool{ - return $this->isTemporal; + public function isTemporary() : bool{ + return $this->state === self::STATE_CONNECTING; } public function isConnected() : bool{ @@ -157,7 +171,7 @@ public function isConnected() : bool{ public function update(float $time) : void{ if(!$this->isActive and ($this->lastUpdate + 10) < $time){ - $this->forciblyDisconnect("timeout"); + $this->forciblyDisconnect(DisconnectReason::PEER_TIMEOUT); return; } @@ -192,7 +206,7 @@ public function update(float $time) : void{ } } - private function queueConnectedPacket(ConnectedPacket $packet, int $reliability, int $orderChannel, bool $immediate = false) : void{ + protected function queueConnectedPacket(ConnectedPacket $packet, int $reliability, int $orderChannel, bool $immediate = false) : void{ $out = new PacketSerializer(); //TODO: reuse streams to reduce allocations $packet->encode($out); @@ -208,61 +222,32 @@ public function addEncapsulatedToQueue(EncapsulatedPacket $packet, bool $immedia $this->sendLayer->addEncapsulatedToQueue($packet, $immediate); } - private function sendPacket(Packet $packet) : void{ - $this->server->sendPacket($packet, $this->address); - } - - private function sendPing(int $reliability = PacketReliability::UNRELIABLE) : void{ - $this->queueConnectedPacket(ConnectedPing::create($this->server->getRakNetTimeMS()), $reliability, 0, true); + protected function sendPing(int $reliability = PacketReliability::UNRELIABLE) : void{ + $this->queueConnectedPacket(ConnectedPing::create($this->getRakNetTimeMS()), $reliability, 0, true); } private function handleEncapsulatedPacketRoute(EncapsulatedPacket $packet) : void{ - if($this->server === null){ - return; - } - $id = ord($packet->buffer[0]); if($id < MessageIdentifiers::ID_USER_PACKET_ENUM){ //internal data packet if($this->state === self::STATE_CONNECTING){ - if($id === ConnectionRequest::$ID){ - $dataPacket = new ConnectionRequest(); - $dataPacket->decode(new PacketSerializer($packet->buffer)); - $this->queueConnectedPacket(ConnectionRequestAccepted::create( - $this->address, - [], - $dataPacket->sendPingTime, - $this->server->getRakNetTimeMS() - ), PacketReliability::UNRELIABLE, 0, true); - }elseif($id === NewIncomingConnection::$ID){ - $dataPacket = new NewIncomingConnection(); - $dataPacket->decode(new PacketSerializer($packet->buffer)); - - if($dataPacket->address->getPort() === $this->server->getPort() or !$this->server->portChecking){ - $this->state = self::STATE_CONNECTED; //FINALLY! - $this->isTemporal = false; - $this->server->openSession($this); - - //$this->handlePong($dataPacket->sendPingTime, $dataPacket->sendPongTime); //can't use this due to system-address count issues in MCPE >.< - $this->sendPing(); - } - } - }elseif($id === DisconnectionNotification::$ID){ - $this->onClientDisconnect(); - }elseif($id === ConnectedPing::$ID){ + $this->handleRakNetConnectionPacket($packet->buffer); + }elseif($id === MessageIdentifiers::ID_DISCONNECTION_NOTIFICATION){ + $this->handleRemoteDisconnect(); + }elseif($id === MessageIdentifiers::ID_CONNECTED_PING){ $dataPacket = new ConnectedPing(); $dataPacket->decode(new PacketSerializer($packet->buffer)); $this->queueConnectedPacket(ConnectedPong::create( $dataPacket->sendPingTime, - $this->server->getRakNetTimeMS() + $this->getRakNetTimeMS() ), PacketReliability::UNRELIABLE, 0); - }elseif($id === ConnectedPong::$ID){ + }elseif($id === MessageIdentifiers::ID_CONNECTED_PONG){ $dataPacket = new ConnectedPong(); $dataPacket->decode(new PacketSerializer($packet->buffer)); $this->handlePong($dataPacket->sendPingTime, $dataPacket->sendPongTime); } }elseif($this->state === self::STATE_CONNECTED){ - $this->server->getEventListener()->onPacketReceive($this->internalId, $packet->buffer); + $this->onPacketReceive($packet->buffer); }else{ //$this->logger->notice("Received packet before connection: " . bin2hex($packet->buffer)); } @@ -272,12 +257,12 @@ private function handleEncapsulatedPacketRoute(EncapsulatedPacket $packet) : voi * @param int $sendPongTime TODO: clock differential stuff */ private function handlePong(int $sendPingTime, int $sendPongTime) : void{ - $currentTime = $this->server->getRakNetTimeMS(); + $currentTime = $this->getRakNetTimeMS(); if($currentTime < $sendPingTime){ $this->logger->debug("Received invalid pong: timestamp is in the future by " . ($sendPingTime - $currentTime) . " ms"); }else{ $this->lastPingMeasure = $currentTime - $sendPingTime; - $this->server->getEventListener()->onPingMeasure($this->internalId, $this->lastPingMeasure); + $this->onPingMeasure($this->lastPingMeasure); } } @@ -296,26 +281,36 @@ public function handlePacket(Packet $packet) : void{ /** * Initiates a graceful asynchronous disconnect which ensures both parties got all packets. + * + * @param int $reason one of the DisconnectReason constants + * @phpstan-param DisconnectReason::* $reason + * + * @see DisconnectReason */ - public function initiateDisconnect(string $reason) : void{ + public function initiateDisconnect(int $reason) : void{ if($this->isConnected()){ $this->state = self::STATE_DISCONNECT_PENDING; $this->disconnectionTime = microtime(true); - $this->server->getEventListener()->onClientDisconnect($this->internalId, $reason); - $this->logger->debug("Requesting graceful disconnect because \"$reason\""); + $this->onDisconnect($reason); + $this->logger->debug("Requesting graceful disconnect because \"" . DisconnectReason::toString($reason) . "\""); } } /** * Disconnects the session with immediate effect, regardless of current session state. Usually used in timeout cases. + * + * @param int $reason one of the DisconnectReason constants + * @phpstan-param DisconnectReason::* $reason + * + * @see DisconnectReason */ - public function forciblyDisconnect(string $reason) : void{ + public function forciblyDisconnect(int $reason) : void{ $this->state = self::STATE_DISCONNECTED; - $this->server->getEventListener()->onClientDisconnect($this->internalId, $reason); - $this->logger->debug("Forcibly disconnecting session due to \"$reason\""); + $this->onDisconnect($reason); + $this->logger->debug("Forcibly disconnecting session due to " . DisconnectReason::toString($reason)); } - private function onClientDisconnect() : void{ + private function handleRemoteDisconnect() : void{ //the client will expect an ACK for this; make sure it gets sent, because after forcible termination //there won't be any session ticks to update it $this->recvLayer->update(); @@ -323,7 +318,7 @@ private function onClientDisconnect() : void{ if($this->isConnected()){ //the client might have disconnected after the server sent a disconnect notification, but before the client //received it - in this case, we don't want to notify the event handler twice - $this->server->getEventListener()->onClientDisconnect($this->internalId, "client disconnect"); + $this->onDisconnect(DisconnectReason::CLIENT_DISCONNECT); } $this->state = self::STATE_DISCONNECTED; $this->logger->debug("Terminating session due to client disconnect"); diff --git a/src/generic/Socket.php b/src/generic/Socket.php index b88bf9a..7f710c9 100644 --- a/src/generic/Socket.php +++ b/src/generic/Socket.php @@ -16,17 +16,13 @@ namespace raklib\generic; -use raklib\utils\InternetAddress; -use function socket_bind; use function socket_close; use function socket_create; use function socket_last_error; -use function socket_recvfrom; -use function socket_sendto; +use function socket_set_block; use function socket_set_nonblock; use function socket_set_option; use function socket_strerror; -use function strlen; use function trim; use const AF_INET; use const AF_INET6; @@ -34,52 +30,28 @@ use const SO_RCVBUF; use const SO_SNDBUF; use const SOCK_DGRAM; -use const SOCKET_EADDRINUSE; -use const SOCKET_EWOULDBLOCK; use const SOL_SOCKET; use const SOL_UDP; -class Socket{ - /** @var \Socket */ - protected $socket; - /** @var InternetAddress */ - private $bindAddress; +abstract class Socket{ + protected \Socket $socket; /** * @throws SocketException */ - public function __construct(InternetAddress $bindAddress){ - $this->bindAddress = $bindAddress; - $socket = @socket_create($bindAddress->getVersion() === 4 ? AF_INET : AF_INET6, SOCK_DGRAM, SOL_UDP); + protected function __construct(bool $ipv6){ + $socket = @socket_create($ipv6 ? AF_INET6 : AF_INET, SOCK_DGRAM, SOL_UDP); if($socket === false){ throw new \RuntimeException("Failed to create socket: " . trim(socket_strerror(socket_last_error()))); } $this->socket = $socket; - if($bindAddress->getVersion() === 6){ + if($ipv6){ socket_set_option($this->socket, IPPROTO_IPV6, IPV6_V6ONLY, 1); //Don't map IPv4 to IPv6, the implementation can create another RakLib instance to handle IPv4 } - - if(@socket_bind($this->socket, $bindAddress->getIp(), $bindAddress->getPort()) === true){ - $this->setSendBuffer(1024 * 1024 * 8)->setRecvBuffer(1024 * 1024 * 8); - }else{ - $error = socket_last_error($this->socket); - if($error === SOCKET_EADDRINUSE){ //platform error messages aren't consistent - throw new SocketException("Failed to bind socket: Something else is already running on $bindAddress", $error); - } - throw new SocketException("Failed to bind to " . $bindAddress . ": " . trim(socket_strerror($error)), $error); - } - socket_set_nonblock($this->socket); } - public function getBindAddress() : InternetAddress{ - return $this->bindAddress; - } - - /** - * @return \Socket - */ - public function getSocket(){ + public function getSocket() : \Socket{ return $this->socket; } @@ -92,40 +64,19 @@ public function getLastError() : int{ } /** - * @param string $source reference parameter - * @param int $port reference parameter - * - * @throws SocketException + * @return $this */ - public function readPacket(?string &$source, ?int &$port) : ?string{ - $buffer = ""; - if(@socket_recvfrom($this->socket, $buffer, 65535, 0, $source, $port) === false){ - $errno = socket_last_error($this->socket); - if($errno === SOCKET_EWOULDBLOCK){ - return null; - } - throw new SocketException("Failed to recv (errno $errno): " . trim(socket_strerror($errno)), $errno); - } - return $buffer; - } + public function setSendBuffer(int $size){ + @socket_set_option($this->socket, SOL_SOCKET, SO_SNDBUF, $size); - /** - * @throws SocketException - */ - public function writePacket(string $buffer, string $dest, int $port) : int{ - $result = @socket_sendto($this->socket, $buffer, strlen($buffer), 0, $dest, $port); - if($result === false){ - $errno = socket_last_error($this->socket); - throw new SocketException("Failed to send to $dest $port (errno $errno): " . trim(socket_strerror($errno)), $errno); - } - return $result; + return $this; } /** * @return $this */ - public function setSendBuffer(int $size){ - @socket_set_option($this->socket, SOL_SOCKET, SO_SNDBUF, $size); + public function setRecvBuffer(int $size){ + @socket_set_option($this->socket, SOL_SOCKET, SO_RCVBUF, $size); return $this; } @@ -133,9 +84,17 @@ public function setSendBuffer(int $size){ /** * @return $this */ - public function setRecvBuffer(int $size){ - @socket_set_option($this->socket, SOL_SOCKET, SO_RCVBUF, $size); + public function setRecvTimeout(int $seconds, int $microseconds = 0){ + @socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, ["sec" => $seconds, "usec" => $microseconds]); return $this; } + + public function setBlocking(bool $blocking) : void{ + if($blocking){ + socket_set_block($this->socket); + }else{ + socket_set_nonblock($this->socket); + } + } } diff --git a/src/protocol/AcknowledgePacket.php b/src/protocol/AcknowledgePacket.php index 654d4aa..6a18733 100644 --- a/src/protocol/AcknowledgePacket.php +++ b/src/protocol/AcknowledgePacket.php @@ -27,7 +27,7 @@ abstract class AcknowledgePacket extends Packet{ private const RECORD_TYPE_SINGLE = 1; /** @var int[] */ - public $packets = []; + public array $packets = []; protected function encodePayload(PacketSerializer $out) : void{ $payload = ""; diff --git a/src/protocol/AdvertiseSystem.php b/src/protocol/AdvertiseSystem.php index a1ecf81..e6beca3 100644 --- a/src/protocol/AdvertiseSystem.php +++ b/src/protocol/AdvertiseSystem.php @@ -19,8 +19,7 @@ class AdvertiseSystem extends Packet{ public static $ID = MessageIdentifiers::ID_ADVERTISE_SYSTEM; - /** @var string */ - public $serverName; + public string $serverName; protected function encodePayload(PacketSerializer $out) : void{ $out->putString($this->serverName); diff --git a/src/protocol/ConnectedPing.php b/src/protocol/ConnectedPing.php index d8280f0..82830af 100644 --- a/src/protocol/ConnectedPing.php +++ b/src/protocol/ConnectedPing.php @@ -19,8 +19,7 @@ class ConnectedPing extends ConnectedPacket{ public static $ID = MessageIdentifiers::ID_CONNECTED_PING; - /** @var int */ - public $sendPingTime; + public int $sendPingTime; public static function create(int $sendPingTime) : self{ $result = new self; diff --git a/src/protocol/ConnectedPong.php b/src/protocol/ConnectedPong.php index b188ccc..bd42c87 100644 --- a/src/protocol/ConnectedPong.php +++ b/src/protocol/ConnectedPong.php @@ -19,10 +19,8 @@ class ConnectedPong extends ConnectedPacket{ public static $ID = MessageIdentifiers::ID_CONNECTED_PONG; - /** @var int */ - public $sendPingTime; - /** @var int */ - public $sendPongTime; + public int $sendPingTime; + public int $sendPongTime; public static function create(int $sendPingTime, int $sendPongTime) : self{ $result = new self; diff --git a/src/protocol/ConnectionRequest.php b/src/protocol/ConnectionRequest.php index 966400c..94a888f 100644 --- a/src/protocol/ConnectionRequest.php +++ b/src/protocol/ConnectionRequest.php @@ -19,12 +19,9 @@ class ConnectionRequest extends ConnectedPacket{ public static $ID = MessageIdentifiers::ID_CONNECTION_REQUEST; - /** @var int */ - public $clientID; - /** @var int */ - public $sendPingTime; - /** @var bool */ - public $useSecurity = false; + public int $clientID; + public int $sendPingTime; + public bool $useSecurity = false; protected function encodePayload(PacketSerializer $out) : void{ $out->putLong($this->clientID); diff --git a/src/protocol/ConnectionRequestAccepted.php b/src/protocol/ConnectionRequestAccepted.php index 8f5775b..275ec96 100644 --- a/src/protocol/ConnectionRequestAccepted.php +++ b/src/protocol/ConnectionRequestAccepted.php @@ -23,15 +23,11 @@ class ConnectionRequestAccepted extends ConnectedPacket{ public static $ID = MessageIdentifiers::ID_CONNECTION_REQUEST_ACCEPTED; - /** @var InternetAddress */ - public $address; + public InternetAddress $address; /** @var InternetAddress[] */ - public $systemAddresses = []; - - /** @var int */ - public $sendPingTime; - /** @var int */ - public $sendPongTime; + public array $systemAddresses = []; + public int $sendPingTime; + public int $sendPongTime; /** * @param InternetAddress[] $systemAddresses diff --git a/src/protocol/Datagram.php b/src/protocol/Datagram.php index ad9a3b7..e93e299 100644 --- a/src/protocol/Datagram.php +++ b/src/protocol/Datagram.php @@ -31,14 +31,10 @@ class Datagram extends Packet{ public const HEADER_SIZE = 1 + 3; //header flags (1) + sequence number (3) - /** @var int */ - public $headerFlags = 0; - + public int $headerFlags = 0; /** @var EncapsulatedPacket[] */ - public $packets = []; - - /** @var int */ - public $seqNumber; + public array $packets = []; + public int $seqNumber; protected function encodeHeader(PacketSerializer $out) : void{ $out->putByte(self::BITFLAG_VALID | $this->headerFlags); diff --git a/src/protocol/EncapsulatedPacket.php b/src/protocol/EncapsulatedPacket.php index 79bdd48..f9bfc85 100644 --- a/src/protocol/EncapsulatedPacket.php +++ b/src/protocol/EncapsulatedPacket.php @@ -29,22 +29,14 @@ class EncapsulatedPacket{ private const SPLIT_FLAG = 0b00010000; - /** @var int */ - public $reliability; - /** @var int|null */ - public $messageIndex; - /** @var int|null */ - public $sequenceIndex; - /** @var int|null */ - public $orderIndex; - /** @var int|null */ - public $orderChannel; - /** @var SplitPacketInfo|null */ - public $splitInfo = null; - /** @var string */ - public $buffer = ""; - /** @var int|null */ - public $identifierACK = null; + public int $reliability; + public ?int $messageIndex = null; + public ?int $sequenceIndex = null; + public ?int $orderIndex = null; + public ?int $orderChannel = null; + public ?SplitPacketInfo $splitInfo = null; + public string $buffer = ""; + public ?int $identifierACK = null; /** * @throws BinaryDataException @@ -54,7 +46,7 @@ public static function fromBinary(BinaryStream $stream) : EncapsulatedPacket{ $flags = $stream->getByte(); $packet->reliability = $reliability = ($flags & self::RELIABILITY_FLAGS) >> self::RELIABILITY_SHIFT; - $hasSplit = ($flags & self::SPLIT_FLAG) > 0; + $hasSplit = ($flags & self::SPLIT_FLAG) !== 0; $length = (int) ceil($stream->getShort() / 8); if($length === 0){ diff --git a/src/protocol/IncompatibleProtocolVersion.php b/src/protocol/IncompatibleProtocolVersion.php index b9b9452..c3575cd 100644 --- a/src/protocol/IncompatibleProtocolVersion.php +++ b/src/protocol/IncompatibleProtocolVersion.php @@ -19,10 +19,8 @@ class IncompatibleProtocolVersion extends OfflineMessage{ public static $ID = MessageIdentifiers::ID_INCOMPATIBLE_PROTOCOL_VERSION; - /** @var int */ - public $protocolVersion; - /** @var int */ - public $serverId; + public int $protocolVersion; + public int $serverId; public static function create(int $protocolVersion, int $serverId) : self{ $result = new self; diff --git a/src/protocol/NewIncomingConnection.php b/src/protocol/NewIncomingConnection.php index 4394ac5..5ae4e55 100644 --- a/src/protocol/NewIncomingConnection.php +++ b/src/protocol/NewIncomingConnection.php @@ -23,16 +23,11 @@ class NewIncomingConnection extends ConnectedPacket{ public static $ID = MessageIdentifiers::ID_NEW_INCOMING_CONNECTION; - /** @var InternetAddress */ - public $address; - + public InternetAddress $address; /** @var InternetAddress[] */ - public $systemAddresses = []; - - /** @var int */ - public $sendPingTime; - /** @var int */ - public $sendPongTime; + public array $systemAddresses = []; + public int $sendPingTime; + public int $sendPongTime; protected function encodePayload(PacketSerializer $out) : void{ $out->putAddress($this->address); diff --git a/src/protocol/OfflineMessage.php b/src/protocol/OfflineMessage.php index dd229f2..d157d0f 100644 --- a/src/protocol/OfflineMessage.php +++ b/src/protocol/OfflineMessage.php @@ -26,8 +26,7 @@ abstract class OfflineMessage extends Packet{ */ private const MAGIC = "\x00\xff\xff\x00\xfe\xfe\xfe\xfe\xfd\xfd\xfd\xfd\x12\x34\x56\x78"; - /** @var string */ - protected $magic; + protected string $magic = self::MAGIC; /** * @return void @@ -41,7 +40,7 @@ protected function readMagic(BinaryStream $in){ * @return void */ protected function writeMagic(BinaryStream $out){ - $out->put(self::MAGIC); + $out->put($this->magic); } public function isValid() : bool{ diff --git a/src/protocol/OpenConnectionReply1.php b/src/protocol/OpenConnectionReply1.php index 22cd0f0..eaa227c 100644 --- a/src/protocol/OpenConnectionReply1.php +++ b/src/protocol/OpenConnectionReply1.php @@ -19,12 +19,9 @@ class OpenConnectionReply1 extends OfflineMessage{ public static $ID = MessageIdentifiers::ID_OPEN_CONNECTION_REPLY_1; - /** @var int */ - public $serverID; - /** @var bool */ - public $serverSecurity = false; - /** @var int */ - public $mtuSize; + public int $serverID; + public bool $serverSecurity = false; + public int $mtuSize; public static function create(int $serverId, bool $serverSecurity, int $mtuSize) : self{ $result = new self; diff --git a/src/protocol/OpenConnectionReply2.php b/src/protocol/OpenConnectionReply2.php index 5f60fe6..f0eff65 100644 --- a/src/protocol/OpenConnectionReply2.php +++ b/src/protocol/OpenConnectionReply2.php @@ -21,14 +21,10 @@ class OpenConnectionReply2 extends OfflineMessage{ public static $ID = MessageIdentifiers::ID_OPEN_CONNECTION_REPLY_2; - /** @var int */ - public $serverID; - /** @var InternetAddress */ - public $clientAddress; - /** @var int */ - public $mtuSize; - /** @var bool */ - public $serverSecurity = false; + public int $serverID; + public InternetAddress $clientAddress; + public int $mtuSize; + public bool $serverSecurity = false; public static function create(int $serverId, InternetAddress $clientAddress, int $mtuSize, bool $serverSecurity) : self{ $result = new self; diff --git a/src/protocol/OpenConnectionRequest1.php b/src/protocol/OpenConnectionRequest1.php index c1d23d6..fc9d8e6 100644 --- a/src/protocol/OpenConnectionRequest1.php +++ b/src/protocol/OpenConnectionRequest1.php @@ -23,10 +23,8 @@ class OpenConnectionRequest1 extends OfflineMessage{ public static $ID = MessageIdentifiers::ID_OPEN_CONNECTION_REQUEST_1; - /** @var int */ - public $protocol = RakLib::DEFAULT_PROTOCOL_VERSION; - /** @var int */ - public $mtuSize; + public int $protocol = RakLib::DEFAULT_PROTOCOL_VERSION; + public int $mtuSize; protected function encodePayload(PacketSerializer $out) : void{ $this->writeMagic($out); diff --git a/src/protocol/OpenConnectionRequest2.php b/src/protocol/OpenConnectionRequest2.php index 0a97e80..bc22183 100644 --- a/src/protocol/OpenConnectionRequest2.php +++ b/src/protocol/OpenConnectionRequest2.php @@ -21,12 +21,9 @@ class OpenConnectionRequest2 extends OfflineMessage{ public static $ID = MessageIdentifiers::ID_OPEN_CONNECTION_REQUEST_2; - /** @var int */ - public $clientID; - /** @var InternetAddress */ - public $serverAddress; - /** @var int */ - public $mtuSize; + public int $clientID; + public InternetAddress $serverAddress; + public int $mtuSize; protected function encodePayload(PacketSerializer $out) : void{ $this->writeMagic($out); diff --git a/src/protocol/SplitPacketInfo.php b/src/protocol/SplitPacketInfo.php index 3fbcf4c..179043c 100644 --- a/src/protocol/SplitPacketInfo.php +++ b/src/protocol/SplitPacketInfo.php @@ -17,18 +17,12 @@ namespace raklib\protocol; final class SplitPacketInfo{ - /** @var int */ - private $id; - /** @var int */ - private $partIndex; - /** @var int */ - private $totalPartCount; - - public function __construct(int $id, int $partIndex, int $totalPartCount){ + public function __construct( + private int $id, + private int $partIndex, + private int $totalPartCount + ){ //TODO: argument validation - $this->id = $id; - $this->partIndex = $partIndex; - $this->totalPartCount = $totalPartCount; } public function getId() : int{ diff --git a/src/protocol/UnconnectedPing.php b/src/protocol/UnconnectedPing.php index 9fb6097..cc47423 100644 --- a/src/protocol/UnconnectedPing.php +++ b/src/protocol/UnconnectedPing.php @@ -19,10 +19,8 @@ class UnconnectedPing extends OfflineMessage{ public static $ID = MessageIdentifiers::ID_UNCONNECTED_PING; - /** @var int */ - public $sendPingTime; - /** @var int */ - public $clientId; + public int $sendPingTime; + public int $clientId; protected function encodePayload(PacketSerializer $out) : void{ $out->putLong($this->sendPingTime); diff --git a/src/protocol/UnconnectedPong.php b/src/protocol/UnconnectedPong.php index c23b6b7..827652d 100644 --- a/src/protocol/UnconnectedPong.php +++ b/src/protocol/UnconnectedPong.php @@ -19,12 +19,9 @@ class UnconnectedPong extends OfflineMessage{ public static $ID = MessageIdentifiers::ID_UNCONNECTED_PONG; - /** @var int */ - public $sendPingTime; - /** @var int */ - public $serverId; - /** @var string */ - public $serverName; + public int $sendPingTime; + public int $serverId; + public string $serverName; public static function create(int $sendPingTime, int $serverId, string $serverName) : self{ $result = new self; diff --git a/src/server/Server.php b/src/server/Server.php index 966463e..95eb9fd 100644 --- a/src/server/Server.php +++ b/src/server/Server.php @@ -17,7 +17,8 @@ namespace raklib\server; use pocketmine\utils\BinaryDataException; -use raklib\generic\Socket; +use raklib\generic\DisconnectReason; +use raklib\generic\Session; use raklib\generic\SocketException; use raklib\protocol\ACK; use raklib\protocol\Datagram; @@ -45,91 +46,54 @@ class Server implements ServerInterface{ private const RAKLIB_TPS = 100; private const RAKLIB_TIME_PER_TICK = 1 / self::RAKLIB_TPS; - /** @var Socket */ - protected $socket; + protected int $receiveBytes = 0; + protected int $sendBytes = 0; - /** @var \Logger */ - protected $logger; + /** @var ServerSession[] */ + protected array $sessionsByAddress = []; + /** @var ServerSession[] */ + protected array $sessions = []; - /** @var int */ - protected $serverId; + protected UnconnectedMessageHandler $unconnectedMessageHandler; - /** @var int */ - protected $receiveBytes = 0; - /** @var int */ - protected $sendBytes = 0; + protected string $name = ""; - /** @var Session[] */ - protected $sessionsByAddress = []; - /** @var Session[] */ - protected $sessions = []; + protected int $packetLimit = 200; - /** @var UnconnectedMessageHandler */ - protected $unconnectedMessageHandler; - /** @var string */ - protected $name = ""; + protected bool $shutdown = false; - /** @var int */ - protected $packetLimit = 200; - - /** @var bool */ - protected $shutdown = false; - - /** @var int */ - protected $ticks = 0; + protected int $ticks = 0; /** @var int[] string (address) => int (unblock time) */ - protected $block = []; + protected array $block = []; /** @var int[] string (address) => int (number of packets) */ - protected $ipSec = []; + protected array $ipSec = []; /** @var string[] regex filters used to block out unwanted raw packets */ - protected $rawPacketFilters = []; - - /** @var bool */ - public $portChecking = false; - - /** @var int */ - protected $startTimeMS; - - /** @var int */ - protected $maxMtuSize; - - /** @var int */ - protected $nextSessionId = 0; - - /** @var ServerEventSource */ - private $eventSource; - /** @var ServerEventListener */ - private $eventListener; - - /** @var ExceptionTraceCleaner */ - private $traceCleaner; - - public function __construct(int $serverId, \Logger $logger, Socket $socket, int $maxMtuSize, ProtocolAcceptor $protocolAcceptor, ServerEventSource $eventSource, ServerEventListener $eventListener, ExceptionTraceCleaner $traceCleaner){ + protected array $rawPacketFilters = []; + + public bool $portChecking = false; + + protected int $nextSessionId = 0; + + public function __construct( + protected int $serverId, + protected \Logger $logger, + protected ServerSocket $socket, + protected int $maxMtuSize, + ProtocolAcceptor $protocolAcceptor, + private ServerEventSource $eventSource, + private ServerEventListener $eventListener, + private ExceptionTraceCleaner $traceCleaner + ){ if($maxMtuSize < Session::MIN_MTU_SIZE){ throw new \InvalidArgumentException("MTU size must be at least " . Session::MIN_MTU_SIZE . ", got $maxMtuSize"); } - $this->serverId = $serverId; - $this->logger = $logger; - $this->socket = $socket; - $this->maxMtuSize = $maxMtuSize; - $this->eventSource = $eventSource; - $this->eventListener = $eventListener; - $this->traceCleaner = $traceCleaner; - - $this->startTimeMS = (int) (microtime(true) * 1000); + $this->socket->setBlocking(false); $this->unconnectedMessageHandler = new UnconnectedMessageHandler($this, $protocolAcceptor); } - /** - * Returns the time in milliseconds since server start. - */ - public function getRakNetTimeMS() : int{ - return ((int) (microtime(true) * 1000)) - $this->startTimeMS; - } - public function getPort() : int{ return $this->socket->getBindAddress()->getPort(); } @@ -182,7 +146,7 @@ public function waitShutdown() : void{ } foreach($this->sessions as $session){ - $session->initiateDisconnect("server shutdown"); + $session->initiateDisconnect(DisconnectReason::SERVER_SHUTDOWN); } while(count($this->sessions) > 0){ @@ -353,7 +317,7 @@ public function sendRaw(string $address, int $port, string $payload) : void{ public function closeSession(int $sessionId) : void{ if(isset($this->sessions[$sessionId])){ - $this->sessions[$sessionId]->initiateDisconnect("server disconnect"); + $this->sessions[$sessionId]->initiateDisconnect(DisconnectReason::SERVER_DISCONNECT); } } @@ -392,7 +356,7 @@ public function addRawPacketFilter(string $regex) : void{ $this->rawPacketFilters[] = $regex; } - public function getSessionByAddress(InternetAddress $address) : ?Session{ + public function getSessionByAddress(InternetAddress $address) : ?ServerSession{ return $this->sessionsByAddress[$address->toString()] ?? null; } @@ -400,10 +364,10 @@ public function sessionExists(InternetAddress $address) : bool{ return isset($this->sessionsByAddress[$address->toString()]); } - public function createSession(InternetAddress $address, int $clientId, int $mtuSize) : Session{ + public function createSession(InternetAddress $address, int $clientId, int $mtuSize) : ServerSession{ $existingSession = $this->sessionsByAddress[$address->toString()] ?? null; if($existingSession !== null){ - $existingSession->forciblyDisconnect("client reconnect"); + $existingSession->forciblyDisconnect(DisconnectReason::CLIENT_RECONNECT); $this->removeSessionInternal($existingSession); } @@ -414,7 +378,7 @@ public function createSession(InternetAddress $address, int $clientId, int $mtuS $this->nextSessionId &= 0x7fffffff; //we don't expect more than 2 billion simultaneous connections, and this fits in 4 bytes } - $session = new Session($this, $this->logger, clone $address, $clientId, $mtuSize, $this->nextSessionId); + $session = new ServerSession($this, $this->logger, clone $address, $clientId, $mtuSize, $this->nextSessionId); $this->sessionsByAddress[$address->toString()] = $session; $this->sessions[$this->nextSessionId] = $session; $this->logger->debug("Created session for $address with MTU size $mtuSize"); @@ -422,11 +386,11 @@ public function createSession(InternetAddress $address, int $clientId, int $mtuS return $session; } - private function removeSessionInternal(Session $session) : void{ + private function removeSessionInternal(ServerSession $session) : void{ unset($this->sessionsByAddress[$session->getAddress()->toString()], $this->sessions[$session->getInternalId()]); } - public function openSession(Session $session) : void{ + public function openSession(ServerSession $session) : void{ $address = $session->getAddress(); $this->eventListener->onClientConnect($session->getInternalId(), $address->getIp(), $address->getPort(), $session->getID()); } @@ -434,7 +398,7 @@ public function openSession(Session $session) : void{ private function checkSessions() : void{ if(count($this->sessions) > 4096){ foreach($this->sessions as $sessionId => $session){ - if($session->isTemporal()){ + if($session->isTemporary()){ $this->removeSessionInternal($session); if(count($this->sessions) <= 4096){ break; @@ -444,10 +408,6 @@ private function checkSessions() : void{ } } - public function notifyACK(Session $session, int $identifierACK) : void{ - $this->eventListener->onPacketAck($session->getInternalId(), $identifierACK); - } - public function getName() : string{ return $this->name; } diff --git a/src/server/ServerEventListener.php b/src/server/ServerEventListener.php index d36cc78..9e2d4d4 100644 --- a/src/server/ServerEventListener.php +++ b/src/server/ServerEventListener.php @@ -16,11 +16,19 @@ namespace raklib\server; +use raklib\generic\DisconnectReason; + interface ServerEventListener{ public function onClientConnect(int $sessionId, string $address, int $port, int $clientID) : void; - public function onClientDisconnect(int $sessionId, string $reason) : void; + /** + * @param int $reason one of the DisconnectReason constants + * @phpstan-param DisconnectReason::* $reason + * + * @see DisconnectReason + */ + public function onClientDisconnect(int $sessionId, int $reason) : void; public function onPacketReceive(int $sessionId, string $packet) : void; diff --git a/src/server/ServerSession.php b/src/server/ServerSession.php new file mode 100644 index 0000000..1c7df86 --- /dev/null +++ b/src/server/ServerSession.php @@ -0,0 +1,91 @@ + + * + * RakLib is not affiliated with Jenkins Software LLC nor RakNet. + * + * RakLib 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, either version 3 of the License, or + * (at your option) any later version. + */ + +declare(strict_types=1); + +namespace raklib\server; + +use raklib\generic\Session; +use raklib\protocol\ConnectionRequest; +use raklib\protocol\ConnectionRequestAccepted; +use raklib\protocol\MessageIdentifiers; +use raklib\protocol\NewIncomingConnection; +use raklib\protocol\Packet; +use raklib\protocol\PacketReliability; +use raklib\protocol\PacketSerializer; +use raklib\utils\InternetAddress; +use function ord; + +class ServerSession extends Session{ + private Server $server; + private int $internalId; + + public function __construct(Server $server, \Logger $logger, InternetAddress $address, int $clientId, int $mtuSize, int $internalId){ + $this->server = $server; + $this->internalId = $internalId; + parent::__construct($logger, $address, $clientId, $mtuSize); + } + + /** + * Returns an ID used to identify this session across threads. + */ + public function getInternalId() : int{ + return $this->internalId; + } + + final protected function sendPacket(Packet $packet) : void{ + $this->server->sendPacket($packet, $this->address); + } + + protected function onPacketAck(int $identifierACK) : void{ + $this->server->getEventListener()->onPacketAck($this->internalId, $identifierACK); + } + + protected function onDisconnect(int $reason) : void{ + $this->server->getEventListener()->onClientDisconnect($this->internalId, $reason); + } + + final protected function handleRakNetConnectionPacket(string $packet) : void{ + $id = ord($packet[0]); + if($id === MessageIdentifiers::ID_CONNECTION_REQUEST){ + $dataPacket = new ConnectionRequest(); + $dataPacket->decode(new PacketSerializer($packet)); + $this->queueConnectedPacket(ConnectionRequestAccepted::create( + $this->address, + [], + $dataPacket->sendPingTime, + $this->getRakNetTimeMS() + ), PacketReliability::UNRELIABLE, 0, true); + }elseif($id === MessageIdentifiers::ID_NEW_INCOMING_CONNECTION){ + $dataPacket = new NewIncomingConnection(); + $dataPacket->decode(new PacketSerializer($packet)); + + if($dataPacket->address->getPort() === $this->server->getPort() or !$this->server->portChecking){ + $this->state = self::STATE_CONNECTED; //FINALLY! + $this->server->openSession($this); + + //$this->handlePong($dataPacket->sendPingTime, $dataPacket->sendPongTime); //can't use this due to system-address count issues in MCPE >.< + $this->sendPing(); + } + } + } + + protected function onPacketReceive(string $packet) : void{ + $this->server->getEventListener()->onPacketReceive($this->internalId, $packet); + } + + protected function onPingMeasure(int $pingMS) : void{ + $this->server->getEventListener()->onPingMeasure($this->internalId, $pingMS); + } +} diff --git a/src/server/ServerSocket.php b/src/server/ServerSocket.php new file mode 100644 index 0000000..0cd7852 --- /dev/null +++ b/src/server/ServerSocket.php @@ -0,0 +1,90 @@ + + * + * RakLib is not affiliated with Jenkins Software LLC nor RakNet. + * + * RakLib 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, either version 3 of the License, or + * (at your option) any later version. + */ + +declare(strict_types=1); + +namespace raklib\server; + +use raklib\generic\Socket; +use raklib\generic\SocketException; +use raklib\utils\InternetAddress; +use function socket_bind; +use function socket_last_error; +use function socket_recvfrom; +use function socket_sendto; +use function socket_set_option; +use function socket_strerror; +use function strlen; +use function trim; + +class ServerSocket extends Socket{ + + public function __construct( + private InternetAddress $bindAddress + ){ + parent::__construct($this->bindAddress->getVersion() === 6); + + if(@socket_bind($this->socket, $this->bindAddress->getIp(), $this->bindAddress->getPort()) === true){ + $this->setSendBuffer(1024 * 1024 * 8)->setRecvBuffer(1024 * 1024 * 8); + }else{ + $error = socket_last_error($this->socket); + if($error === SOCKET_EADDRINUSE){ //platform error messages aren't consistent + throw new SocketException("Failed to bind socket: Something else is already running on $this->bindAddress", $error); + } + throw new SocketException("Failed to bind to " . $this->bindAddress . ": " . trim(socket_strerror($error)), $error); + } + } + + public function getBindAddress() : InternetAddress{ + return $this->bindAddress; + } + + public function enableBroadcast() : bool{ + return socket_set_option($this->socket, SOL_SOCKET, SO_BROADCAST, 1); + } + + public function disableBroadcast() : bool{ + return socket_set_option($this->socket, SOL_SOCKET, SO_BROADCAST, 0); + } + + /** + * @param string $source reference parameter + * @param int $port reference parameter + * + * @throws SocketException + */ + public function readPacket(?string &$source, ?int &$port) : ?string{ + $buffer = ""; + if(@socket_recvfrom($this->socket, $buffer, 65535, 0, $source, $port) === false){ + $errno = socket_last_error($this->socket); + if($errno === SOCKET_EWOULDBLOCK){ + return null; + } + throw new SocketException("Failed to recv (errno $errno): " . trim(socket_strerror($errno)), $errno); + } + return $buffer; + } + + /** + * @throws SocketException + */ + public function writePacket(string $buffer, string $dest, int $port) : int{ + $result = @socket_sendto($this->socket, $buffer, strlen($buffer), 0, $dest, $port); + if($result === false){ + $errno = socket_last_error($this->socket); + throw new SocketException("Failed to send to $dest $port (errno $errno): " . trim(socket_strerror($errno)), $errno); + } + return $result; + } +} diff --git a/src/server/SimpleProtocolAcceptor.php b/src/server/SimpleProtocolAcceptor.php index 9db5253..245bbec 100644 --- a/src/server/SimpleProtocolAcceptor.php +++ b/src/server/SimpleProtocolAcceptor.php @@ -18,12 +18,9 @@ final class SimpleProtocolAcceptor implements ProtocolAcceptor{ - /** @var int */ - private $protocolVersion; - - public function __construct(int $protocolVersion){ - $this->protocolVersion = $protocolVersion; - } + public function __construct( + private int $protocolVersion + ){} public function accepts(int $protocolVersion) : bool{ return $this->protocolVersion === $protocolVersion; diff --git a/src/server/UnconnectedMessageHandler.php b/src/server/UnconnectedMessageHandler.php index 582d97c..2ab7bad 100644 --- a/src/server/UnconnectedMessageHandler.php +++ b/src/server/UnconnectedMessageHandler.php @@ -17,7 +17,9 @@ namespace raklib\server; use pocketmine\utils\BinaryDataException; +use raklib\generic\Session; use raklib\protocol\IncompatibleProtocolVersion; +use raklib\protocol\MessageIdentifiers; use raklib\protocol\OfflineMessage; use raklib\protocol\OpenConnectionReply1; use raklib\protocol\OpenConnectionReply2; @@ -35,20 +37,17 @@ use function substr; class UnconnectedMessageHandler{ - /** @var Server */ - private $server; /** * @var OfflineMessage[]|\SplFixedArray * @phpstan-var \SplFixedArray */ - private $packetPool; - /** @var ProtocolAcceptor */ - private $protocolAcceptor; + private \SplFixedArray $packetPool; - public function __construct(Server $server, ProtocolAcceptor $protocolAcceptor){ + public function __construct( + private Server $server, + private ProtocolAcceptor $protocolAcceptor + ){ $this->registerPackets(); - $this->server = $server; - $this->protocolAcceptor = $protocolAcceptor; } /** @@ -130,10 +129,10 @@ public function getPacketFromPool(string $buffer) : ?OfflineMessage{ private function registerPackets() : void{ $this->packetPool = new \SplFixedArray(256); - $this->registerPacket(UnconnectedPing::$ID, UnconnectedPing::class); - $this->registerPacket(UnconnectedPingOpenConnections::$ID, UnconnectedPingOpenConnections::class); - $this->registerPacket(OpenConnectionRequest1::$ID, OpenConnectionRequest1::class); - $this->registerPacket(OpenConnectionRequest2::$ID, OpenConnectionRequest2::class); + $this->registerPacket(MessageIdentifiers::ID_UNCONNECTED_PING, UnconnectedPing::class); + $this->registerPacket(MessageIdentifiers::ID_UNCONNECTED_PING_OPEN_CONNECTIONS, UnconnectedPingOpenConnections::class); + $this->registerPacket(MessageIdentifiers::ID_OPEN_CONNECTION_REQUEST_1, OpenConnectionRequest1::class); + $this->registerPacket(MessageIdentifiers::ID_OPEN_CONNECTION_REQUEST_2, OpenConnectionRequest2::class); } } diff --git a/src/utils/ExceptionTraceCleaner.php b/src/utils/ExceptionTraceCleaner.php index 55c4fcd..8e71b3a 100644 --- a/src/utils/ExceptionTraceCleaner.php +++ b/src/utils/ExceptionTraceCleaner.php @@ -17,6 +17,7 @@ namespace raklib\utils; use function array_reverse; +use function count; use function function_exists; use function get_class; use function gettype; @@ -28,20 +29,16 @@ use function xdebug_get_function_stack; final class ExceptionTraceCleaner{ - /** @var string */ - private $mainPath; - - public function __construct(string $mainPath){ - $this->mainPath = $mainPath; - } + public function __construct( + private string $mainPath + ){} /** - * @param int $start * @param list>|null $trace * * @return list */ - public function getTrace($start = 0, $trace = null){ + public function getTrace(int $start = 0, ?array $trace = null) : array{ if($trace === null){ if(function_exists("xdebug_get_function_stack") && count($trace = @xdebug_get_function_stack()) !== 0){ $trace = array_reverse($trace); @@ -71,12 +68,7 @@ public function getTrace($start = 0, $trace = null){ return $messages; } - /** - * @param string $path - * - * @return string - */ - public function cleanPath($path){ + public function cleanPath(string $path) : string{ return str_replace(["\\", ".php", "phar://", str_replace(["\\", "phar://"], ["/", ""], $this->mainPath)], ["/", "", "", ""], $path); } } diff --git a/src/utils/InternetAddress.php b/src/utils/InternetAddress.php index f1ba161..4e90961 100644 --- a/src/utils/InternetAddress.php +++ b/src/utils/InternetAddress.php @@ -17,21 +17,14 @@ namespace raklib\utils; final class InternetAddress{ - - /** @var string */ - private $ip; - /** @var int */ - private $port; - /** @var int */ - private $version; - - public function __construct(string $address, int $port, int $version){ - $this->ip = $address; + public function __construct( + private string $ip, + private int $port, + private int $version + ){ if($port < 0 or $port > 65535){ throw new \InvalidArgumentException("Invalid port range"); } - $this->port = $port; - $this->version = $version; } public function getIp() : string{ diff --git a/tools/proxy.php b/tools/proxy.php new file mode 100644 index 0000000..6ef9045 --- /dev/null +++ b/tools/proxy.php @@ -0,0 +1,210 @@ +warning("You may experience problems connecting to PocketMine-MP servers on ports other than 19132 if the server has port checking enabled"); +} + +\GlobalLogger::get()->info("Opening listen socket"); +try{ + $proxyToServerUnconnectedSocket = new ClientSocket(new InternetAddress($serverAddress, $serverPort, 4)); + $proxyToServerUnconnectedSocket->setBlocking(false); +}catch(SocketException $e){ + \GlobalLogger::get()->emergency("Can't connect to $serverAddress on port $serverPort, is the server online?"); + \GlobalLogger::get()->emergency($e->getMessage()); + exit(1); +} +socket_getsockname($proxyToServerUnconnectedSocket->getSocket(), $proxyAddr, $proxyPort); +\GlobalLogger::get()->info("Listening on $bindAddr:$bindPort, sending from $proxyAddr:$proxyPort, sending to $serverAddress:$serverPort"); + +try{ + $clientProxySocket = new ServerSocket(new InternetAddress($bindAddr, $bindPort, 4)); + $clientProxySocket->setBlocking(false); +}catch(SocketException $e){ + \GlobalLogger::get()->emergency("Can't bind to $bindAddr on port $bindPort, is something already using that port?"); + \GlobalLogger::get()->emergency($e->getMessage()); + exit(1); +} + +\GlobalLogger::get()->info("Press CTRL+C to stop the proxy"); + +$clientAddr = $clientPort = null; + +class ClientSession{ + private int $lastUsedTime; + + public function __construct( + private InternetAddress $address, + private ClientSocket $proxyToServerSocket + ){ + $this->lastUsedTime = time(); + } + + public function getAddress() : InternetAddress{ + return $this->address; + } + + public function getSocket() : ClientSocket{ + return $this->proxyToServerSocket; + } + + public function setActive() : void{ + $this->lastUsedTime = time(); + } + + public function isActive() : bool{ + return time() - $this->lastUsedTime < 10; + } +} + +function serverToClientRelay(ClientSession $client, ServerSocket $clientProxySocket) : void{ + $buffer = $client->getSocket()->readPacket(); + if($buffer !== null){ + $clientProxySocket->writePacket($buffer, $client->getAddress()->getIp(), $client->getAddress()->getPort()); + } +} + +/** @var ClientSession[][] $clients */ +$clients = []; + +$serverId = mt_rand(0, Limits::INT32_MAX); +$mostRecentPong = null; + +while(true){ + $k = 0; + $r = []; + $r[++$k] = $clientProxySocket->getSocket(); + $r[++$k] = $proxyToServerUnconnectedSocket->getSocket(); + $clientIndex = []; + foreach($clients as $ipClients){ + foreach($ipClients as $client){ + $key = ++$k; + $r[$key] = $client->getSocket()->getSocket(); + $clientIndex[$key] = $client; + } + } + $w = $e = null; + if(socket_select($r, $w, $e, 10) > 0){ + foreach($r as $key => $socket){ + if(isset($clientIndex[$key])){ + serverToClientRelay($clientIndex[$key], $clientProxySocket); + }elseif($socket === $proxyToServerUnconnectedSocket->getSocket()){ + $buffer = $proxyToServerUnconnectedSocket->readPacket(); + if($buffer !== null && $buffer !== "" && ord($buffer[0]) === MessageIdentifiers::ID_UNCONNECTED_PONG){ + $mostRecentPong = $buffer; + \GlobalLogger::get()->info("Caching ping response from server: " . $buffer); + } + }elseif($socket === $clientProxySocket->getSocket()){ + try{ + $buffer = $clientProxySocket->readPacket($recvAddr, $recvPort); + }catch(SocketException $e){ + $error = $e->getCode(); + if($error === SOCKET_ECONNRESET){ //client disconnected improperly, maybe crash or lost connection + continue; + } + + \GlobalLogger::get()->error("Socket error: " . $e->getMessage()); + continue; + } + + if($buffer === null || $buffer === ""){ + continue; + } + if(isset($clients[$recvAddr][$recvPort])){ + $client = $clients[$recvAddr][$recvPort]; + $client->setActive(); + $client->getSocket()->writePacket($buffer); + }elseif(ord($buffer[0]) === MessageIdentifiers::ID_UNCONNECTED_PING){ + \GlobalLogger::get()->info("Got ping from $recvAddr on port $recvPort, pinging server"); + $proxyToServerUnconnectedSocket->writePacket($buffer); + + if($mostRecentPong !== null){ + $clientProxySocket->writePacket($mostRecentPong, $recvAddr, $recvPort); + }else{ + \GlobalLogger::get()->info("No cached ping response, waiting for server to respond"); + } + }elseif(ord($buffer[0]) === MessageIdentifiers::ID_OPEN_CONNECTION_REQUEST_1){ + \GlobalLogger::get()->info("Got connection from $recvAddr on port $recvPort"); + $proxyToServerUnconnectedSocket->writePacket($buffer); + $client = new ClientSession(new InternetAddress($recvAddr, $recvPort, 4), new ClientSocket(new InternetAddress($serverAddress, $serverPort, 4))); + $client->getSocket()->setBlocking(false); + $clients[$recvAddr][$recvPort] = $client; + socket_getsockname($client->getSocket()->getSocket(), $proxyAddr, $proxyPort); + \GlobalLogger::get()->info("Established connection: $recvAddr:$recvPort <-> $proxyAddr:$proxyPort <-> $serverAddress:$serverPort"); + }else{ + \GlobalLogger::get()->warning("Unexpected packet from unconnected client $recvAddr on port $recvPort: " . bin2hex($buffer)); + } + }else{ + throw new \LogicException("Unexpected socket in select result"); + } + } + } + foreach($clients as $ip => $ipClients){ + foreach($ipClients as $port => $client){ + if(!$client->isActive()){ + \GlobalLogger::get()->info("Closing session for client $ip:$port"); + unset($clients[$ip][$port]); + } + } + } +} \ No newline at end of file diff --git a/tools/scan.php b/tools/scan.php new file mode 100644 index 0000000..1f3a6f0 --- /dev/null +++ b/tools/scan.php @@ -0,0 +1,58 @@ + " . PHP_EOL; + exit(1); +} + +if(str_contains($broadcastAddress, ".")){ + $bindAddress = new InternetAddress("0.0.0.0", 0, 4); +}else{ + $bindAddress = new InternetAddress("::", 0, 6); +} + +$socket = new ServerSocket($bindAddress); +$socket->enableBroadcast(); +$clientId = mt_rand(0, Limits::INT32_MAX); +\GlobalLogger::get()->info("Listening on " . $bindAddress); +\GlobalLogger::get()->info("Press CTRL+C to stop"); + +function sendPing(ServerSocket $socket, string $broadcastAddress, int $port, int $clientId) : void{ + $ping = new UnconnectedPing(); + $ping->clientId = $clientId; + $ping->sendPingTime = intdiv(hrtime(true), 1_000_000); + + $serializer = new PacketSerializer(); + $ping->encode($serializer); + $socket->writePacket($serializer->getBuffer(), $broadcastAddress, $port); +} +sendPing($socket, $broadcastAddress, $port, $clientId); + +socket_set_option($socket->getSocket(), SOL_SOCKET, SO_RCVTIMEO, ["sec" => 1, "usec" => 0]); +while(true){ //@phpstan-ignore-line + try{ + $pong = $socket->readPacket($serverIp, $serverPort); + if($pong !== null && ord($pong[0]) === MessageIdentifiers::ID_UNCONNECTED_PONG){ + \GlobalLogger::get()->info("Pong received from $serverIp:$serverPort: " . $pong); + } + }catch(SocketException $e){ + if($e->getCode() === SOCKET_ETIMEDOUT){ + sendPing($socket, $broadcastAddress, $port, $clientId); + } + } +} \ No newline at end of file