PocketMine-MP 5.30.2 git-98f04176111e5ecab5e8814ffc69d992bfb64939
Loading...
Searching...
No Matches
vendor/pocketmine/raklib/src/server/Server.php
1<?php
2
3/*
4 * This file is part of RakLib.
5 * Copyright (C) 2014-2022 PocketMine Team <https://github.com/pmmp/RakLib>
6 *
7 * RakLib is not affiliated with Jenkins Software LLC nor RakNet.
8 *
9 * RakLib is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
13 */
14
15declare(strict_types=1);
16
17namespace raklib\server;
18
32use function asort;
33use function assert;
34use function bin2hex;
35use function count;
36use function get_class;
37use function microtime;
38use function ord;
39use function preg_match;
40use function strlen;
41use function time;
42use function time_sleep_until;
43use const PHP_INT_MAX;
44use const SOCKET_ECONNRESET;
45
46class Server implements ServerInterface{
47
48 private const RAKLIB_TPS = 100;
49 private const RAKLIB_TIME_PER_TICK = 1 / self::RAKLIB_TPS;
50 private const BLOCK_MESSAGE_SUPPRESSION_THRESHOLD = 2;
51 private const PACKET_ERROR_SUPPRESSION_THRESHOLD = 2;
52
53 protected int $receiveBytes = 0;
54 protected int $sendBytes = 0;
55
57 protected array $sessionsByAddress = [];
59 protected array $sessions = [];
60
61 protected UnconnectedMessageHandler $unconnectedMessageHandler;
62
63 protected string $name = "";
64
65 protected int $packetLimit = 200;
66
67 protected bool $shutdown = false;
68
69 protected int $ticks = 0;
70
72 protected array $block = [];
74 protected array $ipSec = [];
75
76 private int $blockedSinceLastUpdate = 0;
77
78 private int $packetErrorsSinceLastUpdate = 0;
79
81 protected array $rawPacketFilters = [];
82
83 public bool $portChecking = false;
84
85 protected int $nextSessionId = 0;
86
91 public function __construct(
92 protected int $serverId,
93 protected \Logger $logger,
94 protected ServerSocket $socket,
95 protected int $maxMtuSize,
96 ProtocolAcceptor $protocolAcceptor,
97 private ServerEventSource $eventSource,
98 private ServerEventListener $eventListener,
99 private ExceptionTraceCleaner $traceCleaner,
100 private int $recvMaxSplitParts = ServerSession::DEFAULT_MAX_SPLIT_PART_COUNT,
101 private int $recvMaxConcurrentSplits = ServerSession::DEFAULT_MAX_CONCURRENT_SPLIT_COUNT,
102 private int $blockMessageSuppressionThreshold = self::BLOCK_MESSAGE_SUPPRESSION_THRESHOLD,
103 private int $packetErrorSuppressionThreshold = self::PACKET_ERROR_SUPPRESSION_THRESHOLD,
104 private bool $blockIpOnPacketErrors = true
105 ){
106 if($maxMtuSize < Session::MIN_MTU_SIZE){
107 throw new \InvalidArgumentException("MTU size must be at least " . Session::MIN_MTU_SIZE . ", got $maxMtuSize");
108 }
109 $this->socket->setBlocking(false);
110
111 $this->unconnectedMessageHandler = new UnconnectedMessageHandler($this, $protocolAcceptor);
112 }
113
114 public function getPort() : int{
115 return $this->socket->getBindAddress()->getPort();
116 }
117
118 public function getMaxMtuSize() : int{
119 return $this->maxMtuSize;
120 }
121
122 public function getLogger() : \Logger{
123 return $this->logger;
124 }
125
126 public function tickProcessor() : void{
127 $start = microtime(true);
128
129 /*
130 * The below code is designed to allow co-op between sending and receiving to avoid slowing down either one
131 * when high traffic is coming either way. Yielding will occur after 100 messages.
132 */
133 do{
134 $stream = !$this->shutdown;
135 for($i = 0; $i < 100 && $stream && !$this->shutdown; ++$i){ //if we received a shutdown event, we don't care about any more messages from the event source
136 $stream = $this->eventSource->process($this);
137 }
138
139 $socket = true;
140 for($i = 0; $i < 100 && $socket; ++$i){
141 $socket = $this->receivePacket();
142 }
143 }while($stream || $socket);
144
145 $this->tick();
146
147 $time = microtime(true) - $start;
148 if($time < self::RAKLIB_TIME_PER_TICK){
149 @time_sleep_until(microtime(true) + self::RAKLIB_TIME_PER_TICK - $time);
150 }
151 }
152
156 public function waitShutdown() : void{
157 $this->shutdown = true;
158
159 while($this->eventSource->process($this)){
160 //Ensure that any late messages are processed before we start initiating server disconnects, so that if the
161 //server implementation used a custom disconnect mechanism (e.g. a server transfer), we don't break it in
162 //race conditions.
163 }
164
165 foreach($this->sessions as $session){
166 $session->initiateDisconnect(DisconnectReason::SERVER_SHUTDOWN);
167 }
168
169 while(count($this->sessions) > 0){
170 $this->tickProcessor();
171 }
172
173 $this->socket->close();
174 $this->logger->debug("Graceful shutdown complete");
175 }
176
177 private function tick() : void{
178 $time = microtime(true);
179 foreach($this->sessions as $session){
180 $session->update($time);
181 if($session->isFullyDisconnected()){
182 $this->removeSessionInternal($session);
183 }
184 }
185
186 $this->ipSec = [];
187
188 if(!$this->shutdown and ($this->ticks % self::RAKLIB_TPS) === 0){
189 if($this->sendBytes > 0 or $this->receiveBytes > 0){
190 $this->eventListener->onBandwidthStatsUpdate($this->sendBytes, $this->receiveBytes);
191 $this->sendBytes = 0;
192 $this->receiveBytes = 0;
193 }
194
195 $packetErrorsWithoutMessage = $this->packetErrorsSinceLastUpdate - $this->packetErrorSuppressionThreshold;
196 if($packetErrorsWithoutMessage > 0){
197 $this->logger->warning("$packetErrorsWithoutMessage suppressed packet errors - RakLib may be under attack");
198 }
199 $this->packetErrorsSinceLastUpdate = 0;
200
201 $ipsBlockedWithoutMessage = $this->blockedSinceLastUpdate - $this->blockMessageSuppressionThreshold;
202 if($ipsBlockedWithoutMessage > 0){
203 $this->logger->warning("$ipsBlockedWithoutMessage more IP addresses were blocked - RakLib may be under attack");
204 }
205 $this->blockedSinceLastUpdate = 0;
206
207 if(count($this->block) > 0){
208 asort($this->block);
209 $now = time();
210 foreach($this->block as $address => $timeout){
211 if($timeout <= $now){
212 unset($this->block[$address]);
213 }else{
214 break;
215 }
216 }
217 }
218 }
219
220 ++$this->ticks;
221 }
222
224 private function receivePacket() : bool{
225 try{
226 $buffer = $this->socket->readPacket($addressIp, $addressPort);
227 }catch(SocketException $e){
228 $error = $e->getCode();
229 if($error === SOCKET_ECONNRESET){ //client disconnected improperly, maybe crash or lost connection
230 return true;
231 }
232
233 $this->logger->debug($e->getMessage());
234 return false;
235 }
236 if($buffer === null){
237 return false; //no data
238 }
239 assert($addressIp !== null, "Can't be null if we got a buffer");
240 assert($addressPort !== null, "Can't be null if we got a buffer");
241
242 $len = strlen($buffer);
243
244 $this->receiveBytes += $len;
245 if(isset($this->block[$addressIp])){
246 return true;
247 }
248
249 if(isset($this->ipSec[$addressIp])){
250 if(++$this->ipSec[$addressIp] >= $this->packetLimit){
251 $this->blockAddress($addressIp);
252 return true;
253 }
254 }else{
255 $this->ipSec[$addressIp] = 1;
256 }
257
258 if($len < 1){
259 return true;
260 }
261
262 $address = new InternetAddress($addressIp, $addressPort, $this->socket->getBindAddress()->getVersion());
263 try{
264 $session = $this->getSessionByAddress($address);
265 if($session !== null){
266 $header = ord($buffer[0]);
267 if(($header & Datagram::BITFLAG_VALID) !== 0){
268 if(($header & Datagram::BITFLAG_ACK) !== 0){
269 $packet = new ACK();
270 }elseif(($header & Datagram::BITFLAG_NAK) !== 0){
271 $packet = new NACK();
272 }else{
273 $packet = new Datagram();
274 }
275 $packet->decode(new PacketSerializer($buffer));
276 try{
277 $session->handlePacket($packet);
278 }catch(PacketHandlingException $e){
279 $session->getLogger()->error("Error receiving packet: " . $e->getMessage());
280 $session->forciblyDisconnect($e->getDisconnectReason());
281 }
282 return true;
283 }elseif($session->isConnected()){
284 //allows unconnected packets if the session is stuck in DISCONNECTING state, useful if the client
285 //didn't disconnect properly for some reason (e.g. crash)
286 $this->logger->debug("Ignored unconnected packet from $address due to session already opened (0x" . bin2hex($buffer[0]) . ")");
287 return true;
288 }
289 }
290
291 if(!$this->shutdown){
292 if(!($handled = $this->unconnectedMessageHandler->handleRaw($buffer, $address))){
293 foreach($this->rawPacketFilters as $pattern){
294 if(preg_match($pattern, $buffer) > 0){
295 $handled = true;
296 $this->eventListener->onRawPacketReceive($address->getIp(), $address->getPort(), $buffer);
297 break;
298 }
299 }
300 }
301
302 if(!$handled){
303 $this->logger->debug("Ignored packet from $address due to no session opened (0x" . bin2hex($buffer[0]) . ")");
304 }
305 }
306 }catch(BinaryDataException $e){
307 if($this->packetErrorsSinceLastUpdate < $this->packetErrorSuppressionThreshold){
308 $logFn = function() use ($address, $e, $buffer) : void{
309 $this->logger->debug("Packet from $address (" . strlen($buffer) . " bytes): 0x" . bin2hex($buffer));
310 $this->logger->debug(get_class($e) . ": " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine());
311 foreach($this->traceCleaner->getTrace(0, $e->getTrace()) as $line){
312 $this->logger->debug($line);
313 }
314 $this->logger->error("Bad packet from $address: " . $e->getMessage());
315 };
316 if($this->logger instanceof \BufferedLogger){
317 $this->logger->buffer($logFn);
318 }else{
319 $logFn();
320 }
321 }
322 $this->packetErrorsSinceLastUpdate++;
323 if($this->blockIpOnPacketErrors){
324 $this->blockAddress($address->getIp(), 5);
325 }
326 }
327
328 return true;
329 }
330
331 public function sendPacket(Packet $packet, InternetAddress $address) : void{
332 $out = new PacketSerializer(); //TODO: reusable streams to reduce allocations
333 $packet->encode($out);
334 try{
335 $this->sendBytes += $this->socket->writePacket($out->getBuffer(), $address->getIp(), $address->getPort());
336 }catch(SocketException $e){
337 $this->logger->debug($e->getMessage());
338 }
339 }
340
341 public function getEventListener() : ServerEventListener{
342 return $this->eventListener;
343 }
344
345 public function sendEncapsulated(int $sessionId, EncapsulatedPacket $packet, bool $immediate = false) : void{
346 $session = $this->sessions[$sessionId] ?? null;
347 if($session !== null and $session->isConnected()){
348 $session->addEncapsulatedToQueue($packet, $immediate);
349 }
350 }
351
352 public function sendRaw(string $address, int $port, string $payload) : void{
353 try{
354 $this->socket->writePacket($payload, $address, $port);
355 }catch(SocketException $e){
356 $this->logger->debug($e->getMessage());
357 }
358 }
359
360 public function closeSession(int $sessionId) : void{
361 if(isset($this->sessions[$sessionId])){
362 $this->sessions[$sessionId]->initiateDisconnect(DisconnectReason::SERVER_DISCONNECT);
363 }
364 }
365
366 public function setName(string $name) : void{
367 $this->name = $name;
368 }
369
370 public function setPortCheck(bool $value) : void{
371 $this->portChecking = $value;
372 }
373
374 public function setPacketsPerTickLimit(int $limit) : void{
375 $this->packetLimit = $limit;
376 }
377
378 public function blockAddress(string $address, int $timeout = 300) : void{
379 $final = time() + $timeout;
380 if(!isset($this->block[$address]) or $timeout === -1){
381 if($timeout === -1){
382 $final = PHP_INT_MAX;
383 }
384 if($this->blockedSinceLastUpdate < $this->blockMessageSuppressionThreshold){
385 //Suppress additional log messages if multiple IPs have been banned in quick succession
386 //In the case of IP spoofing attacks we don't want log spam to slow down the server
387 $this->logger->notice("Blocked $address" . ($timeout === -1 ? " forever" : " for $timeout seconds"));
388 }
389 $this->block[$address] = $final;
390 $this->blockedSinceLastUpdate++;
391 }elseif($this->block[$address] < $final){
392 $this->block[$address] = $final;
393 }
394 }
395
396 public function unblockAddress(string $address) : void{
397 unset($this->block[$address]);
398 $this->logger->debug("Unblocked $address");
399 }
400
401 public function addRawPacketFilter(string $regex) : void{
402 $this->rawPacketFilters[] = $regex;
403 }
404
405 public function getSessionByAddress(InternetAddress $address) : ?ServerSession{
406 return $this->sessionsByAddress[$address->toString()] ?? null;
407 }
408
409 public function sessionExists(InternetAddress $address) : bool{
410 return isset($this->sessionsByAddress[$address->toString()]);
411 }
412
413 public function createSession(InternetAddress $address, int $clientId, int $mtuSize) : ServerSession{
414 $existingSession = $this->sessionsByAddress[$address->toString()] ?? null;
415 if($existingSession !== null){
416 $existingSession->forciblyDisconnect(DisconnectReason::CLIENT_RECONNECT);
417 $this->removeSessionInternal($existingSession);
418 }
419
420 $this->checkSessions();
421
422 while(isset($this->sessions[$this->nextSessionId])){
423 $this->nextSessionId++;
424 $this->nextSessionId &= 0x7fffffff; //we don't expect more than 2 billion simultaneous connections, and this fits in 4 bytes
425 }
426
427 $session = new ServerSession($this, $this->logger, clone $address, $clientId, $mtuSize, $this->nextSessionId, $this->recvMaxSplitParts, $this->recvMaxConcurrentSplits);
428 $this->sessionsByAddress[$address->toString()] = $session;
429 $this->sessions[$this->nextSessionId] = $session;
430 $this->logger->debug("Created session for $address with MTU size $mtuSize");
431
432 return $session;
433 }
434
435 private function removeSessionInternal(ServerSession $session) : void{
436 unset($this->sessionsByAddress[$session->getAddress()->toString()], $this->sessions[$session->getInternalId()]);
437 }
438
439 public function openSession(ServerSession $session) : void{
440 $address = $session->getAddress();
441 $this->eventListener->onClientConnect($session->getInternalId(), $address->getIp(), $address->getPort(), $session->getID());
442 }
443
444 private function checkSessions() : void{
445 if(count($this->sessions) > 4096){
446 foreach($this->sessions as $sessionId => $session){
447 if($session->isTemporary()){
448 $this->removeSessionInternal($session);
449 if(count($this->sessions) <= 4096){
450 break;
451 }
452 }
453 }
454 }
455 }
456
457 public function getName() : string{
458 return $this->name;
459 }
460
461 public function getID() : int{
462 return $this->serverId;
463 }
464}
__construct(protected int $serverId, protected \Logger $logger, protected ServerSocket $socket, protected int $maxMtuSize, ProtocolAcceptor $protocolAcceptor, private ServerEventSource $eventSource, private ServerEventListener $eventListener, private ExceptionTraceCleaner $traceCleaner, private int $recvMaxSplitParts=ServerSession::DEFAULT_MAX_SPLIT_PART_COUNT, private int $recvMaxConcurrentSplits=ServerSession::DEFAULT_MAX_CONCURRENT_SPLIT_COUNT, private int $blockMessageSuppressionThreshold=self::BLOCK_MESSAGE_SUPPRESSION_THRESHOLD, private int $packetErrorSuppressionThreshold=self::PACKET_ERROR_SUPPRESSION_THRESHOLD, private bool $blockIpOnPacketErrors=true)