diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9dc6b4d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,48 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for Xdebug", + "type": "php", + "request": "launch", + "port": 9003 + }, + { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 0, + "runtimeArgs": [ + "-dxdebug.start_with_request=yes" + ], + "env": { + "XDEBUG_MODE": "debug,develop", + "XDEBUG_CONFIG": "client_port=${port}" + } + }, + { + "name": "Launch Built-in web server", + "type": "php", + "request": "launch", + "runtimeArgs": [ + "-dxdebug.mode=debug", + "-dxdebug.start_with_request=yes", + "-S", + "localhost:0" + ], + "program": "", + "cwd": "${workspaceRoot}", + "port": 9003, + "serverReadyAction": { + "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started", + "uriFormat": "http://localhost:%s", + "action": "openExternally" + } + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1065b7f --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Stun - A pure PHP async STUN implementation + +Created by Daniil Gentili ([@danog](https://github.com/danog)). + +This is a pure PHP async STUN implementation. + +Usage: + +```bash +composer require danog/stun +``` \ No newline at end of file diff --git a/bind.php b/bind.php new file mode 100644 index 0000000..8396b7d --- /dev/null +++ b/bind.php @@ -0,0 +1,8 @@ +bind()); \ No newline at end of file diff --git a/composer.json b/composer.json index f86ee2c..7bf7821 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ "license": "AGPLv3", "autoload": { "psr-4": { - "Danog\\Stun\\": "src/" + "danog\\Stun\\": "src/" } }, "authors": [ diff --git a/src/Attribute.php b/src/Attribute.php index f10463e..21a5218 100644 --- a/src/Attribute.php +++ b/src/Attribute.php @@ -15,6 +15,15 @@ use danog\Stun\Attributes\XorMappedAddress; use Webmozart\Assert\Assert; abstract class Attribute { + /** + * @internal + */ + public static function posmod(int $a, int $b): int + { + $resto = $a % $b; + return $resto < 0 ? $resto + \abs($b) : $resto; + } + public function write(string $transactionId): string { $data = $this->writeAttr($transactionId); return pack('n', $this::TYPE).strlen($data).$data.str_repeat("\0", 4 - (strlen($data) % 4)); @@ -23,7 +32,7 @@ abstract class Attribute { $totalLength -= 4; Assert::true($totalLength >= 0); - $r = unpack('n', $reader->readLength(4, $cancellation)); + $r = unpack('n*', $reader->readLength(4, $cancellation)); $type = $r[1]; $length = $r[2]; $result = match ($type) { @@ -37,7 +46,8 @@ abstract class Attribute { default => null, }; - $totalLength -= $length + (4 - ($length%4)); + $left = self::posmod($length, 4); + $totalLength -= $length + $left; Assert::true($totalLength >= 0); if ($result) { @@ -45,7 +55,8 @@ abstract class Attribute { } else { $reader->readLength($length, $cancellation); } - $reader->readLength(4 - ($length % 4), $cancellation); + if ($left) + $reader->readLength($left, $cancellation); return $result; } diff --git a/src/Attributes/MappedAddress.php b/src/Attributes/MappedAddress.php index 0e3dac3..719e8cb 100644 --- a/src/Attributes/MappedAddress.php +++ b/src/Attributes/MappedAddress.php @@ -27,12 +27,13 @@ final class MappedAddress extends Attribute { { Assert::true($length >= 8, "Wrong length!"); $reader->readLength(1, $cancellation); - $ip = $reader->readLength($len = match (ord($reader->readLength(1, $cancellation))) { + $len = match (ord($reader->readLength(1, $cancellation))) { 1 => 4, 2 => 16 - }, $cancellation); + }; Assert::eq($len+4, $length, "Wrong length!"); $port = unpack('n', $reader->readLength(2, $cancellation))[1]; + $ip = $reader->readLength($len); return new self(new InternetAddress( inet_ntop($ip), $port @@ -41,6 +42,6 @@ final class MappedAddress extends Attribute { protected function writeAttr(string $_): string { $addr = $this->address->getAddressBytes(); - return "\0".(strlen($addr) === 4 ? 1 : 16).$addr.pack('n', $this->address->getPort()); + return "\0".(strlen($addr) === 4 ? 1 : 16).pack('n', $this->address->getPort()).$addr; } } \ No newline at end of file diff --git a/src/Attributes/XorMappedAddress.php b/src/Attributes/XorMappedAddress.php index 4eac540..b9f6e6f 100644 --- a/src/Attributes/XorMappedAddress.php +++ b/src/Attributes/XorMappedAddress.php @@ -27,14 +27,15 @@ final class XorMappedAddress extends Attribute { protected static function readAttr(BufferedReader $reader, string $transactionId, int $length, ?Cancellation $cancellation = null): self { - Assert::true($length >= 8, "Wrong length!"); + Assert::greaterThanEq($length, 8); $reader->readLength(1, $cancellation); - $ip = match ($len = ord($reader->readLength(1, $cancellation))) { - 1 => $reader->readLength(4, $cancellation) ^ Message::MAGIC_COOKIE, - 2 => $reader->readLength(16, $cancellation) ^ (Message::MAGIC_COOKIE.$transactionId) - }; + $type = ord($reader->readLength(1, $cancellation)); $port = unpack('n', $reader->readLength(2, $cancellation) ^ substr(Message::MAGIC_COOKIE, 2))[1]; - Assert::eq($len+4, $length, "Wrong length!"); + $ip = match ($type) { + 1 => $reader->readLength($len = 4, $cancellation) ^ Message::MAGIC_COOKIE, + 2 => $reader->readLength($len = 16, $cancellation) ^ (Message::MAGIC_COOKIE.$transactionId) + }; + Assert::eq($len+4, $length); return new self(new InternetAddress( inet_ntop($ip), $port @@ -48,6 +49,6 @@ final class XorMappedAddress extends Attribute { } else { $addr ^= Message::MAGIC_COOKIE.$transactionId; } - return "\0".(strlen($addr) === 4 ? 1 : 16).$addr.(pack('n', $this->address->getPort()) ^ substr(Message::MAGIC_COOKIE, 2)); + return "\0".(strlen($addr) === 4 ? 1 : 16).(pack('n', $this->address->getPort()) ^ substr(Message::MAGIC_COOKIE, 2)).$addr; } } \ No newline at end of file diff --git a/src/StunClient.php b/src/StunClient.php index 37bb315..e4dfe7b 100644 --- a/src/StunClient.php +++ b/src/StunClient.php @@ -2,6 +2,9 @@ namespace danog\Stun; +use Amp\ByteStream\BufferedReader; +use Amp\ByteStream\ReadableBuffer; +use Amp\Socket\InternetAddress; use Amp\Socket\Socket; use function Amp\Socket\connect; @@ -25,7 +28,10 @@ final class StunClient { /** * @return list */ - public function bind(Attribute ...$attributes): array { - + public function bind(Attribute ...$attributes): Message { + $msg = new Message(MessageMethod::BINDING, MessageClass::REQUEST, $attributes, random_bytes(12)); + $msg->write($this->socket); + $read = new ReadableBuffer($this->socket->read()); + return Message::read(new BufferedReader($read)); } } \ No newline at end of file