"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); if($response === null){ throw new \RuntimeException("Unable to access XML: {$err}"); } if($response->getCode() !== 200){ throw new \RuntimeException("Unable to access XML: {$response->getBody()}"); } $defaultInternalError = libxml_use_internal_errors(true); try{ $root = new \SimpleXMLElement($response->getBody()); }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; } /** @var string */ private $ip; /** @var int */ private $port; /** @var string|null */ private $serviceURL = null; /** @var \Logger */ private $logger; public function __construct(\Logger $logger, string $ip, int $port){ if(!Internet::$online){ throw new \RuntimeException("Server is offline"); } $this->ip = $ip; $this->port = $port; $this->logger = new \PrefixedLogger($logger, "UPnP Port Forwarder"); } public function start() : void{ $this->serviceURL = self::getServiceUrl(); $body = '' . '' . '' . $this->port . '' . 'UDP' . '' . $this->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($this->serviceURL, $contents, 3, $headers, $err) === null){ throw new \RuntimeException("Failed to portforward using UPnP: " . $err); } $this->logger->info("Forwarded $this->ip:$this->port to external port $this->port"); } public function setName(string $name) : void{ } public function tick() : void{ } public function shutdown() : void{ if($this->serviceURL === null){ return; } $body = '' . '' . '' . $this->port . '' . 'UDP' . ''; $contents = '' . '' . '' . $body . ''; $headers = [ 'Content-Type: text/xml', 'SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping"' ]; Internet::postURL($this->serviceURL, $contents, 3, $headers); } }