"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"); } /** * @throws UPnPException */ public static function getServiceUrl() : string{ $socket = Utils::assumeNotFalse(@socket_create(AF_INET, SOCK_DGRAM, SOL_UDP), fn() => "Socket error: " . trim(socket_strerror(socket_last_error()))); Utils::assumeNotFalse(@socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ["sec" => 3, "usec" => 0]), "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 UPnPException("Socket error: " . trim(socket_strerror(socket_last_error($socket)))); } if($sendbyte !== strlen($contents)){ throw new UPnPException("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 UPnPException("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 UPnPException("Unable to find the router. Ensure that network discovery is enabled in Control Panel."); } $url = parse_url($location); if($url === false){ throw new UPnPException("Failed to parse the router's url: {$location}"); } if(!isset($url['host'])){ throw new UPnPException("Failed to recognize the host name from the router's url: {$location}"); } $urlHost = $url['host']; if(!isset($url['port'])){ throw new UPnPException("Failed to recognize the port number from the router's url: {$location}"); } $urlPort = $url['port']; $err = ""; $response = Internet::getURL($location, 3, [], $err); if($response === null){ throw new UPnPException("Unable to access XML: {$err}"); } if($response->getCode() !== 200){ throw new UPnPException("Unable to access XML: {$response->getBody()}"); } $defaultInternalError = libxml_use_internal_errors(true); try{ $root = new \SimpleXMLElement($response->getBody()); }catch(\Exception $e){ throw new UPnPException("Broken XML."); } libxml_use_internal_errors($defaultInternalError); $root->registerXPathNamespace("upnp", "urn:schemas-upnp-org:device-1-0"); $xpathResult = Utils::assumeNotFalse($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' ), "xpath query is borked"); if($xpathResult === null || count($xpathResult) === 0){ throw new UPnPException("Your router does not support portforwarding"); } $controlURL = (string) $xpathResult[0]; $serviceURL = sprintf("%s:%d/%s", $urlHost, $urlPort, $controlURL); return $serviceURL; } /** * @throws UPnPException */ public static function portForward(string $serviceURL, string $internalIP, int $internalPort, int $externalPort) : void{ $body = '' . '' . '' . $externalPort . '' . 'UDP' . '' . $internalPort . '' . '' . $internalIP . '' . '1' . 'PocketMine-MP' . '0' . ''; $contents = '' . '' . '' . $body . ''; $headers = [ 'Content-Type: text/xml', 'SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"' ]; if(Internet::postURL($serviceURL, $contents, 3, $headers, $err) === null){ throw new UPnPException("Failed to portforward using UPnP: " . $err); } } public static function removePortForward(string $serviceURL, int $externalPort) : void{ $body = '' . '' . '' . $externalPort . '' . 'UDP' . ''; $contents = '' . '' . '' . $body . ''; $headers = [ 'Content-Type: text/xml', 'SOAPAction: "urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping"' ]; Internet::postURL($serviceURL, $contents, 3, $headers); } }