PocketMine-MP 5.15.1 git-ed158f8a1b0cfe334ac5f45febc0f633602014f2
RakLibInterface.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\network\mcpe\raklib;
25
26use pmmp\thread\ThreadSafeArray;
39use pocketmine\player\GameMode;
53use function addcslashes;
54use function base64_encode;
55use function implode;
56use function mt_rand;
57use function rtrim;
58use function substr;
59use const PHP_INT_MAX;
60
66 private const MCPE_RAKNET_PROTOCOL_VERSION = 11;
67
68 private const MCPE_RAKNET_PACKET_ID = "\xfe";
69
70 private Server $server;
71 private Network $network;
72
73 private int $rakServerId;
74 private RakLibServer $rakLib;
75
77 private array $sessions = [];
78
79 private RakLibToUserThreadMessageReceiver $eventReceiver;
80 private UserToRakLibThreadMessageSender $interface;
81
82 private int $sleeperNotifierId;
83
84 private PacketBroadcaster $packetBroadcaster;
85 private EntityEventBroadcaster $entityEventBroadcaster;
86 private TypeConverter $typeConverter;
87
88 public function __construct(
89 Server $server,
90 string $ip,
91 int $port,
92 bool $ipV6,
93 PacketBroadcaster $packetBroadcaster,
94 EntityEventBroadcaster $entityEventBroadcaster,
95 TypeConverter $typeConverter
96 ){
97 $this->server = $server;
98 $this->packetBroadcaster = $packetBroadcaster;
99 $this->entityEventBroadcaster = $entityEventBroadcaster;
100 $this->typeConverter = $typeConverter;
101
102 $this->rakServerId = mt_rand(0, PHP_INT_MAX);
103
104 $sleeperEntry = $this->server->getTickSleeper()->addNotifier(function() : void{
105 Timings::$connection->startTiming();
106 try{
107 while($this->eventReceiver->handle($this));
108 }finally{
109 Timings::$connection->stopTiming();
110 }
111 });
112 $this->sleeperNotifierId = $sleeperEntry->getNotifierId();
113
115 $mainToThreadBuffer = new ThreadSafeArray();
117 $threadToMainBuffer = new ThreadSafeArray();
118
119 $this->rakLib = new RakLibServer(
120 $this->server->getLogger(),
121 $mainToThreadBuffer,
122 $threadToMainBuffer,
123 new InternetAddress($ip, $port, $ipV6 ? 6 : 4),
124 $this->rakServerId,
125 $this->server->getConfigGroup()->getPropertyInt(YmlServerProperties::NETWORK_MAX_MTU_SIZE, 1492),
126 self::MCPE_RAKNET_PROTOCOL_VERSION,
127 $sleeperEntry
128 );
129 $this->eventReceiver = new RakLibToUserThreadMessageReceiver(
130 new PthreadsChannelReader($threadToMainBuffer)
131 );
132 $this->interface = new UserToRakLibThreadMessageSender(
133 new PthreadsChannelWriter($mainToThreadBuffer)
134 );
135 }
136
137 public function start() : void{
138 $this->server->getLogger()->debug("Waiting for RakLib to start...");
139 try{
140 $this->rakLib->startAndWait();
141 }catch(SocketException $e){
142 throw new NetworkInterfaceStartException($e->getMessage(), 0, $e);
143 }
144 $this->server->getLogger()->debug("RakLib booted successfully");
145 }
146
147 public function setNetwork(Network $network) : void{
148 $this->network = $network;
149 }
150
151 public function tick() : void{
152 if(!$this->rakLib->isRunning()){
153 $e = $this->rakLib->getCrashInfo();
154 if($e !== null){
155 throw new ThreadCrashException("RakLib crashed", $e);
156 }
157 throw new \Exception("RakLib Thread crashed without crash information");
158 }
159 }
160
161 public function onClientDisconnect(int $sessionId, int $reason) : void{
162 if(isset($this->sessions[$sessionId])){
163 $session = $this->sessions[$sessionId];
164 unset($this->sessions[$sessionId]);
165 $session->onClientDisconnect(match($reason){
166 DisconnectReason::CLIENT_DISCONNECT => KnownTranslationFactory::pocketmine_disconnect_clientDisconnect(),
167 DisconnectReason::PEER_TIMEOUT => KnownTranslationFactory::pocketmine_disconnect_error_timeout(),
168 DisconnectReason::CLIENT_RECONNECT => KnownTranslationFactory::pocketmine_disconnect_clientReconnect(),
169 default => "Unknown RakLib disconnect reason (ID $reason)"
170 });
171 }
172 }
173
174 public function close(int $sessionId) : void{
175 if(isset($this->sessions[$sessionId])){
176 unset($this->sessions[$sessionId]);
177 $this->interface->closeSession($sessionId);
178 }
179 }
180
181 public function shutdown() : void{
182 $this->server->getTickSleeper()->removeNotifier($this->sleeperNotifierId);
183 $this->rakLib->quit();
184 }
185
186 public function onClientConnect(int $sessionId, string $address, int $port, int $clientID) : void{
187 $session = new NetworkSession(
188 $this->server,
189 $this->network->getSessionManager(),
190 PacketPool::getInstance(),
191 new RakLibPacketSender($sessionId, $this),
192 $this->packetBroadcaster,
193 $this->entityEventBroadcaster,
194 ZlibCompressor::getInstance(), //TODO: this shouldn't be hardcoded, but we might need the RakNet protocol version to select it
195 $this->typeConverter,
196 $address,
197 $port
198 );
199 $this->sessions[$sessionId] = $session;
200 }
201
202 public function onPacketReceive(int $sessionId, string $packet) : void{
203 if(isset($this->sessions[$sessionId])){
204 if($packet === "" || $packet[0] !== self::MCPE_RAKNET_PACKET_ID){
205 $this->sessions[$sessionId]->getLogger()->debug("Non-FE packet received: " . base64_encode($packet));
206 return;
207 }
208 //get this now for blocking in case the player was closed before the exception was raised
209 $session = $this->sessions[$sessionId];
210 $address = $session->getIp();
211 $buf = substr($packet, 1);
212 $name = $session->getDisplayName();
213 try{
214 $session->handleEncoded($buf);
215 }catch(PacketHandlingException $e){
216 $logger = $session->getLogger();
217
218 $session->disconnectWithError(
219 reason: "Bad packet: " . $e->getMessage(),
220 disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error_badPacket()
221 );
222 //intentionally doesn't use logException, we don't want spammy packet error traces to appear in release mode
223 $logger->debug(implode("\n", Utils::printableExceptionInfo($e)));
224
225 $this->interface->blockAddress($address, 5);
226 }catch(\Throwable $e){
227 //record the name of the player who caused the crash, to make it easier to find the reproducing steps
228 $this->server->getLogger()->emergency("Crash occurred while handling a packet from session: $name");
229 throw $e;
230 }
231 }
232 }
233
234 public function blockAddress(string $address, int $timeout = 300) : void{
235 $this->interface->blockAddress($address, $timeout);
236 }
237
238 public function unblockAddress(string $address) : void{
239 $this->interface->unblockAddress($address);
240 }
241
242 public function onRawPacketReceive(string $address, int $port, string $payload) : void{
243 $this->network->processRawPacket($this, $address, $port, $payload);
244 }
245
246 public function sendRawPacket(string $address, int $port, string $payload) : void{
247 $this->interface->sendRaw($address, $port, $payload);
248 }
249
250 public function addRawPacketFilter(string $regex) : void{
251 $this->interface->addRawPacketFilter($regex);
252 }
253
254 public function onPacketAck(int $sessionId, int $identifierACK) : void{
255 if(isset($this->sessions[$sessionId])){
256 $this->sessions[$sessionId]->handleAckReceipt($identifierACK);
257 }
258 }
259
260 public function setName(string $name) : void{
261 $info = $this->server->getQueryInformation();
262
263 $this->interface->setName(implode(";",
264 [
265 "MCPE",
266 rtrim(addcslashes($name, ";"), '\\'),
267 ProtocolInfo::CURRENT_PROTOCOL,
268 ProtocolInfo::MINECRAFT_VERSION_NETWORK,
269 $info->getPlayerCount(),
270 $info->getMaxPlayerCount(),
271 $this->rakServerId,
272 $this->server->getName(),
273 match($this->server->getGamemode()){
274 GameMode::SURVIVAL => "Survival",
275 GameMode::ADVENTURE => "Adventure",
276 default => "Creative"
277 }
278 ]) . ";"
279 );
280 }
281
282 public function setPortCheck(bool $name) : void{
283 $this->interface->setPortCheck($name);
284 }
285
286 public function setPacketLimit(int $limit) : void{
287 $this->interface->setPacketsPerTickLimit($limit);
288 }
289
290 public function onBandwidthStatsUpdate(int $bytesSentDiff, int $bytesReceivedDiff) : void{
291 $this->network->getBandwidthTracker()->add($bytesSentDiff, $bytesReceivedDiff);
292 }
293
294 public function putPacket(int $sessionId, string $payload, bool $immediate = true, ?int $receiptId = null) : void{
295 if(isset($this->sessions[$sessionId])){
296 $pk = new EncapsulatedPacket();
297 $pk->buffer = self::MCPE_RAKNET_PACKET_ID . $payload;
298 $pk->reliability = PacketReliability::RELIABLE_ORDERED;
299 $pk->orderChannel = 0;
300 $pk->identifierACK = $receiptId;
301
302 $this->interface->sendEncapsulated($sessionId, $pk, $immediate);
303 }
304 }
305
306 public function onPingMeasure(int $sessionId, int $pingMS) : void{
307 if(isset($this->sessions[$sessionId])){
308 $this->sessions[$sessionId]->updatePing($pingMS);
309 }
310 }
311}
blockAddress(string $address, int $timeout=300)
onClientDisconnect(int $sessionId, int $reason)
sendRawPacket(string $address, int $port, string $payload)