"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);
}
}