server = $server; $this->interface = $interface; $this->ip = $ip; $this->port = $port; $this->compressedQueue = new \SplQueue(); $this->connectTime = time(); $this->server->getNetwork()->scheduleSessionTick($this); //TODO: this should happen later in the login sequence $this->createPlayer(); $this->setHandler(new LoginSessionHandler($this->player, $this)); } protected function createPlayer() : void{ $ev = new PlayerCreationEvent($this); $ev->call(); $class = $ev->getPlayerClass(); /** * @var Player $player * @see Player::__construct() */ $this->player = new $class($this->server, $this); $this->server->addPlayer($this->player); } public function getPlayer() : ?Player{ return $this->player; } public function isConnected() : bool{ return $this->connected; } public function getInterface() : NetworkInterface{ return $this->interface; } /** * @return string */ public function getIp() : string{ return $this->ip; } /** * @return int */ public function getPort() : int{ return $this->port; } /** * Returns the last recorded ping measurement for this session, in milliseconds. * * @return int */ public function getPing() : int{ return $this->ping; } /** * @internal Called by the network interface to update last recorded ping measurements. * * @param int $ping */ public function updatePing(int $ping) : void{ $this->ping = $ping; } public function getHandler() : SessionHandler{ return $this->handler; } public function setHandler(SessionHandler $handler) : void{ $this->handler = $handler; $this->handler->setUp(); } public function handleEncoded(string $payload) : void{ if(!$this->connected){ return; } if($this->cipher !== null){ Timings::$playerNetworkReceiveDecryptTimer->startTiming(); try{ $payload = $this->cipher->decrypt($payload); }catch(\InvalidArgumentException $e){ $this->server->getLogger()->debug("Encrypted packet from " . $this->ip . " " . $this->port . ": " . bin2hex($payload)); $this->disconnect("Packet decryption error: " . $e->getMessage()); return; }finally{ Timings::$playerNetworkReceiveDecryptTimer->stopTiming(); } } Timings::$playerNetworkReceiveDecompressTimer->startTiming(); try{ $stream = new PacketStream(NetworkCompression::decompress($payload)); }catch(\ErrorException $e){ $this->server->getLogger()->debug("Failed to decompress packet from " . $this->ip . " " . $this->port . ": " . bin2hex($payload)); $this->disconnect("Compressed packet batch decode error (incompatible game version?)", false); return; }finally{ Timings::$playerNetworkReceiveDecompressTimer->stopTiming(); } while(!$stream->feof() and $this->connected){ $this->handleDataPacket(PacketPool::getPacket($stream->getString())); } } public function handleDataPacket(DataPacket $packet) : void{ $timings = Timings::getReceiveDataPacketTimings($packet); $timings->startTiming(); $packet->decode(); if(!$packet->feof() and !$packet->mayHaveUnreadBytes()){ $remains = substr($packet->getBuffer(), $packet->getOffset()); $this->server->getLogger()->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": 0x" . bin2hex($remains)); } $ev = new DataPacketReceiveEvent($this->player, $packet); $ev->call(); if(!$ev->isCancelled() and !$packet->handle($this->handler)){ $this->server->getLogger()->debug("Unhandled " . $packet->getName() . " received from " . $this->player->getName() . ": 0x" . bin2hex($packet->getBuffer())); } $timings->stopTiming(); } public function sendDataPacket(DataPacket $packet, bool $immediate = false) : bool{ $timings = Timings::getSendDataPacketTimings($packet); $timings->startTiming(); try{ $ev = new DataPacketSendEvent($this->player, $packet); $ev->call(); if($ev->isCancelled()){ return false; } $this->addToSendBuffer($packet); if($immediate){ $this->flushSendBuffer(true); } return true; }finally{ $timings->stopTiming(); } } /** * @internal * @param DataPacket $packet */ public function addToSendBuffer(DataPacket $packet) : void{ $timings = Timings::getSendDataPacketTimings($packet); $timings->startTiming(); try{ if($this->sendBuffer === null){ $this->sendBuffer = new PacketStream(); } $this->sendBuffer->putPacket($packet); $this->server->getNetwork()->scheduleSessionTick($this); }finally{ $timings->stopTiming(); } } private function flushSendBuffer(bool $immediate = false) : void{ if($this->sendBuffer !== null){ $promise = $this->server->prepareBatch($this->sendBuffer, $immediate); $this->sendBuffer = null; $this->queueCompressed($promise, $immediate); } } public function queueCompressed(CompressBatchPromise $payload, bool $immediate = false) : void{ $this->flushSendBuffer($immediate); //Maintain ordering if possible if($immediate){ //Skips all queues $this->sendEncoded($payload->getResult(), true); }else{ $this->compressedQueue->enqueue($payload); $payload->onResolve(function(CompressBatchPromise $payload) : void{ if($this->connected and $this->compressedQueue->bottom() === $payload){ $this->compressedQueue->dequeue(); //result unused $this->sendEncoded($payload->getResult()); while(!$this->compressedQueue->isEmpty()){ /** @var CompressBatchPromise $current */ $current = $this->compressedQueue->bottom(); if($current->hasResult()){ $this->compressedQueue->dequeue(); $this->sendEncoded($current->getResult()); }else{ //can't send any more queued until this one is ready break; } } } }); } } private function sendEncoded(string $payload, bool $immediate = false) : void{ if($this->cipher !== null){ Timings::$playerNetworkSendEncryptTimer->startTiming(); $payload = $this->cipher->encrypt($payload); Timings::$playerNetworkSendEncryptTimer->stopTiming(); } $this->interface->putPacket($this, $payload, $immediate); } /** * Disconnects the session, destroying the associated player (if it exists). * * @param string $reason * @param bool $notify */ public function disconnect(string $reason, bool $notify = true) : void{ if($this->connected){ $this->connected = false; $this->player->close($this->player->getLeaveMessage(), $reason); $this->doServerDisconnect($reason, $notify); } } /** * Called by the Player when it is closed (for example due to getting kicked). * * @param string $reason * @param bool $notify */ public function onPlayerDestroyed(string $reason, bool $notify = true) : void{ if($this->connected){ $this->connected = false; $this->doServerDisconnect($reason, $notify); } } /** * Internal helper function used to handle server disconnections. * * @param string $reason * @param bool $notify */ private function doServerDisconnect(string $reason, bool $notify = true) : void{ if($notify){ $pk = new DisconnectPacket(); $pk->message = $reason; $pk->hideDisconnectionScreen = $reason === ""; $this->sendDataPacket($pk, true); } $this->interface->close($this, $notify ? $reason : ""); $this->disconnectCleanup(); } /** * Called by the network interface to close the session when the client disconnects without server input, for * example in a timeout condition or voluntary client disconnect. * * @param string $reason */ public function onClientDisconnect(string $reason) : void{ if($this->connected){ $this->connected = false; $this->player->close($this->player->getLeaveMessage(), $reason); $this->disconnectCleanup(); } } private function disconnectCleanup() : void{ $this->handler = null; $this->interface = null; $this->player = null; $this->sendBuffer = null; $this->compressedQueue = null; } public function enableEncryption(string $encryptionKey, string $handshakeJwt) : void{ $pk = new ServerToClientHandshakePacket(); $pk->jwt = $handshakeJwt; $this->sendDataPacket($pk, true); //make sure this gets sent before encryption is enabled $this->cipher = new NetworkCipher($encryptionKey); $this->setHandler(new HandshakeSessionHandler($this)); $this->server->getLogger()->debug("Enabled encryption for $this->ip $this->port"); } public function onLoginSuccess() : void{ $pk = new PlayStatusPacket(); $pk->status = PlayStatusPacket::LOGIN_SUCCESS; $this->sendDataPacket($pk); $this->player->onLoginSuccess(); $this->setHandler(new ResourcePacksSessionHandler($this->player, $this, $this->server->getResourcePackManager())); } public function onResourcePacksDone() : void{ $this->player->_actuallyConstruct(); $this->setHandler(new PreSpawnSessionHandler($this->server, $this->player, $this)); } public function onTerrainReady() : void{ $pk = new PlayStatusPacket(); $pk->status = PlayStatusPacket::PLAYER_SPAWN; $this->sendDataPacket($pk); } public function onSpawn() : void{ $this->setHandler(new SimpleSessionHandler($this->player)); } public function onDeath() : void{ $this->setHandler(new DeathSessionHandler($this->player, $this)); } public function onRespawn() : void{ $this->setHandler(new SimpleSessionHandler($this->player)); } public function tick() : bool{ if($this->handler instanceof LoginSessionHandler){ if(time() >= $this->connectTime + 10){ $this->disconnect("Login timeout"); return false; } return true; //keep ticking until timeout } if($this->sendBuffer !== null){ $this->flushSendBuffer(); } return false; } }