From edcf2960866baea6c815c1d5841893aae1f82d03 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sun, 14 Mar 2021 20:35:17 +0000 Subject: [PATCH 1/2] RakLibInterface: fixed server being unjoinable if gamemode is Spectator closes #4069 this happens because the client bans any server that has an invalid pong, which is very stupid in this case because the gamemode isn't even shown on the UI anyway ... --- src/pocketmine/network/mcpe/RakLibInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pocketmine/network/mcpe/RakLibInterface.php b/src/pocketmine/network/mcpe/RakLibInterface.php index 5f3ec921a..4097340b4 100644 --- a/src/pocketmine/network/mcpe/RakLibInterface.php +++ b/src/pocketmine/network/mcpe/RakLibInterface.php @@ -212,7 +212,7 @@ class RakLibInterface implements ServerInstance, AdvancedSourceInterface{ $info->getMaxPlayerCount(), $this->rakLib->getServerId(), $this->server->getName(), - Server::getGamemodeName($this->server->getGamemode()) + Server::getGamemodeName(Player::getClientFriendlyGamemode($this->server->getGamemode())) ]) . ";" ); } From cbc8576d4a623bd1b78314bf5937a9529aaf41b3 Mon Sep 17 00:00:00 2001 From: Yosshi999 Date: Mon, 15 Mar 2021 07:50:33 +0900 Subject: [PATCH 2/2] Implement UPnP support without dotNET (#3378) UPnP forwarding is now available on all supported platforms. com_dotnet is no longer required for UPnP forwarding to work. Closes #3216 . --- composer.json | 1 + composer.lock | 3 +- phpstan.neon.dist | 1 - src/pocketmine/network/upnp/UPnP.php | 239 +++++++++++++++++--- src/pocketmine/resources/pocketmine.yml | 2 +- tests/phpstan/configs/com-dotnet-magic.neon | 7 - 6 files changed, 211 insertions(+), 42 deletions(-) delete mode 100644 tests/phpstan/configs/com-dotnet-magic.neon diff --git a/composer.json b/composer.json index 89f7f99ea..dc38aa48e 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "ext-phar": "*", "ext-pthreads": "~3.2.0", "ext-reflection": "*", + "ext-simplexml": "*", "ext-sockets": "*", "ext-spl": "*", "ext-yaml": ">=2.0.0", diff --git a/composer.lock b/composer.lock index c492757c2..34154cd9f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ef9b64ade88849af6ccd0db733e106b5", + "content-hash": "c731270da66df8e18ea9487d91bc2e3f", "packages": [ { "name": "adhocore/json-comment", @@ -2753,6 +2753,7 @@ "ext-phar": "*", "ext-pthreads": "~3.2.0", "ext-reflection": "*", + "ext-simplexml": "*", "ext-sockets": "*", "ext-spl": "*", "ext-yaml": ">=2.0.0", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 7586d7e11..84a7d0141 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,7 +1,6 @@ includes: - tests/phpstan/configs/actual-problems.neon - tests/phpstan/configs/check-explicit-mixed-baseline.neon - - tests/phpstan/configs/com-dotnet-magic.neon - tests/phpstan/configs/gc-hacks.neon - tests/phpstan/configs/l7-baseline.neon - tests/phpstan/configs/l8-baseline.neon diff --git a/src/pocketmine/network/upnp/UPnP.php b/src/pocketmine/network/upnp/UPnP.php index 6fe5a6951..54d5a824c 100644 --- a/src/pocketmine/network/upnp/UPnP.php +++ b/src/pocketmine/network/upnp/UPnP.php @@ -19,63 +19,238 @@ * */ +// This code is based on a Go implementation and its license is below: +// Copyright (c) 2010 Jack Palevich. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + declare(strict_types=1); /** - * UPnP port forwarding support. Only for Windows + * UPnP port forwarding support. */ namespace pocketmine\network\upnp; +use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Internet; -use pocketmine\utils\Utils; -use function class_exists; -use function is_object; +use function count; +use function libxml_use_internal_errors; +use function parse_url; +use function preg_last_error; +use function preg_match; +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_option; +use function socket_strerror; +use function sprintf; +use function strlen; +use function trim; +use const AF_INET; +use const SO_RCVTIMEO; +use const SOCK_DGRAM; +use const SOCKET_ETIMEDOUT; +use const SOL_SOCKET; +use const SOL_UDP; abstract class UPnP{ + private const MAX_DISCOVERY_ATTEMPTS = 3; + + /** @var string|null */ + private static $serviceURL = null; + + private static function makePcreError() : \RuntimeException{ + $errorCode = preg_last_error(); + $message = [ + PREG_INTERNAL_ERROR => "Internal error", + PREG_BACKTRACK_LIMIT_ERROR => "Backtrack limit reached", + PREG_RECURSION_LIMIT_ERROR => "Recursion limit reached", + PREG_BAD_UTF8_ERROR => "Malformed UTF-8", + PREG_BAD_UTF8_OFFSET_ERROR => "Bad UTF-8 offset", + PREG_JIT_STACKLIMIT_ERROR => "PCRE JIT stack limit reached" + ][$errorCode] ?? "Unknown (code $errorCode)"; + throw new \RuntimeException("PCRE error: $message"); + } + + public static function getServiceUrl() : string{ + $socket = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + if($socket === false){ + throw new \RuntimeException("Socket error: " . trim(socket_strerror(socket_last_error()))); + } + if(!@socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ["sec" => 3, "usec" => 0])){ + throw new \RuntimeException("Socket error: " . trim(socket_strerror(socket_last_error($socket)))); + } + $contents = + "M-SEARCH * HTTP/1.1\r\n" . + "MX: 2\r\n" . + "HOST: 239.255.255.250:1900\r\n" . + "MAN: \"ssdp:discover\"\r\n" . + "ST: upnp:rootdevice\r\n\r\n"; + $location = null; + for($i = 0; $i < self::MAX_DISCOVERY_ATTEMPTS; ++$i){ + $sendbyte = @socket_sendto($socket, $contents, strlen($contents), 0, "239.255.255.250", 1900); + if($sendbyte === false){ + throw new \RuntimeException("Socket error: " . trim(socket_strerror(socket_last_error($socket)))); + } + if($sendbyte !== strlen($contents)){ + throw new \RuntimeException("Socket error: Unable to send the entire contents."); + } + while(true){ + if(@socket_recvfrom($socket, $buffer, 1024, 0, $responseHost, $responsePort) === false){ + if(socket_last_error($socket) === SOCKET_ETIMEDOUT){ + continue 2; + } + throw new \RuntimeException("Socket error: " . trim(socket_strerror(socket_last_error($socket)))); + } + $pregResult = preg_match('/location\s*:\s*(.+)\n/i', $buffer, $matches); + if($pregResult === false){ + //TODO: replace with preg_last_error_msg() in PHP 8. + throw self::makePcreError(); + } + if($pregResult !== 0){ //this might be garbage from somewhere other than the router + $location = trim($matches[1]); + break 2; + } + } + } + socket_close($socket); + if($location === null){ + throw new \RuntimeException("Unable to find the router. Ensure that network discovery is enabled in Control Panel."); + } + $url = parse_url($location); + if($url === false){ + throw new \RuntimeException("Failed to parse the router's url: {$location}"); + } + if(!isset($url['host'])){ + throw new \RuntimeException("Failed to recognize the host name from the router's url: {$location}"); + } + $urlHost = $url['host']; + if(!isset($url['port'])){ + throw new \RuntimeException("Failed to recognize the port number from the router's url: {$location}"); + } + $urlPort = $url['port']; + $response = Internet::getURL($location, 3, [], $err, $headers, $httpCode); + if($response === false){ + throw new \RuntimeException("Unable to access XML: {$err}"); + } + if($httpCode !== 200){ + throw new \RuntimeException("Unable to access XML: {$response}"); + } + + $defaultInternalError = libxml_use_internal_errors(true); + try{ + $root = new \SimpleXMLElement($response); + }catch(\Exception $e){ + throw new \RuntimeException("Broken XML."); + } + libxml_use_internal_errors($defaultInternalError); + $root->registerXPathNamespace("upnp", "urn:schemas-upnp-org:device-1-0"); + $xpathResult = $root->xpath( + '//upnp:device[upnp:deviceType="urn:schemas-upnp-org:device:InternetGatewayDevice:1"]' . + '/upnp:deviceList/upnp:device[upnp:deviceType="urn:schemas-upnp-org:device:WANDevice:1"]' . + '/upnp:deviceList/upnp:device[upnp:deviceType="urn:schemas-upnp-org:device:WANConnectionDevice:1"]' . + '/upnp:serviceList/upnp:service[upnp:serviceType="urn:schemas-upnp-org:service:WANIPConnection:1"]' . + '/upnp:controlURL' + ); + if($xpathResult === false){ + //this should be an array of 0 if there is no matching elements; false indicates a problem with the query itself + throw new AssumptionFailedError("xpath query should not error here"); + } + if(count($xpathResult) === 0){ + throw new \RuntimeException("Your router does not support portforwarding"); + } + $controlURL = (string) $xpathResult[0]; + $serviceURL = sprintf("%s:%d/%s", $urlHost, $urlPort, $controlURL); + return $serviceURL; + } public static function PortForward(int $port) : void{ if(!Internet::$online){ throw new \RuntimeException("Server is offline"); } - if(Utils::getOS() !== Utils::OS_WINDOWS){ - throw new \RuntimeException("UPnP is only supported on Windows"); + + if(self::$serviceURL === null){ + self::$serviceURL = self::getServiceUrl(); } - if(!class_exists("COM")){ - throw new \RuntimeException("UPnP requires the com_dotnet extension"); + $body = + '' . + '' . + '' . $port . '' . + 'UDP' . + '' . $port . '' . + '' . Internet::getInternalIP() . '' . + '1' . + 'PocketMine-MP' . + '0' . + ''; + + $contents = + '' . + '' . + '' . $body . ''; + + $headers = [ + 'Content-Type: text/xml', + 'SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"' + ]; + + if(Internet::postURL(self::$serviceURL, $contents, 3, $headers, $err) === false){ + throw new \RuntimeException("Failed to portforward using UPnP: " . $err); } - - $myLocalIP = Internet::getInternalIP(); - - /** @noinspection PhpUndefinedClassInspection */ - $com = new \COM("HNetCfg.NATUPnP"); - /** @noinspection PhpUndefinedFieldInspection */ - - if(!is_object($com->StaticPortMappingCollection)){ - throw new \RuntimeException("Failed to portforward using UPnP. Ensure that network discovery is enabled in Control Panel."); - } - - /** @noinspection PhpUndefinedFieldInspection */ - $com->StaticPortMappingCollection->Add($port, "UDP", $port, $myLocalIP, true, "PocketMine-MP"); } public static function RemovePortForward(int $port) : bool{ if(!Internet::$online){ return false; } - if(Utils::getOS() !== Utils::OS_WINDOWS or !class_exists("COM")){ + if(self::$serviceURL === null){ return false; } - try{ - /** @noinspection PhpUndefinedClassInspection */ - $com = new \COM("HNetCfg.NATUPnP"); - /** @noinspection PhpUndefinedFieldInspection */ - if(!is_object($com->StaticPortMappingCollection)){ - return false; - } - /** @noinspection PhpUndefinedFieldInspection */ - $com->StaticPortMappingCollection->Remove($port, "UDP"); - }catch(\Throwable $e){ + $body = + '' . + '' . + '' . $port . '' . + 'UDP' . + ''; + + $contents = + '' . + '' . + '' . $body . ''; + + $headers = [ + 'Content-Type: text/xml', + 'SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping"' + ]; + + if(Internet::postURL(self::$serviceURL, $contents, 3, $headers) === false){ return false; } diff --git a/src/pocketmine/resources/pocketmine.yml b/src/pocketmine/resources/pocketmine.yml index a513e7993..666da755b 100644 --- a/src/pocketmine/resources/pocketmine.yml +++ b/src/pocketmine/resources/pocketmine.yml @@ -87,7 +87,7 @@ network: compression-level: 6 #Use AsyncTasks for compression. Adds half/one tick delay, less CPU load on main thread async-compression: false - #Experimental, only for Windows. Tries to use UPnP to automatically port forward + #Experimental. Use UPnP to automatically port forward upnp-forwarding: false #Maximum size in bytes of packets sent over the network (default 1492 bytes). Packets larger than this will be #fragmented or split into smaller parts. Clients can request MTU sizes up to but not more than this number. diff --git a/tests/phpstan/configs/com-dotnet-magic.neon b/tests/phpstan/configs/com-dotnet-magic.neon deleted file mode 100644 index bb0f2cbad..000000000 --- a/tests/phpstan/configs/com-dotnet-magic.neon +++ /dev/null @@ -1,7 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Access to an undefined property COM\\:\\:\\$StaticPortMappingCollection\\.$#" - count: 2 - path: ../../../src/pocketmine/network/upnp/UPnP.php -