diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b234b1..d873f5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [Playernames -> UUIDs](https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs) endpoint. - [Change Skin](https://wiki.vg/Mojang_API#Change_Skin) endpoint. - [Reset Skin](https://wiki.vg/Mojang_API#Reset_Skin) endpoint. +- [Blocked Servers](https://wiki.vg/Mojang_API#Blocked_Servers) endpoint. ### Changed - The constructor no longer has arguments. diff --git a/src/Api.php b/src/Api.php index 28189a7..55de5d1 100644 --- a/src/Api.php +++ b/src/Api.php @@ -246,6 +246,20 @@ class Api { ]); } + /** + * @return Response\BlockedServersCollection + * + * @throws GuzzleException + * + * @url https://wiki.vg/Mojang_API#Blocked_Servers + */ + public function blockedServers(): Response\BlockedServersCollection { + $response = $this->getClient()->request('GET', 'https://sessionserver.mojang.com/blockedservers'); + $hashes = explode("\n", trim($response->getBody()->getContents())); + + return new Response\BlockedServersCollection($hashes); + } + /** * @param string $login * @param string $password diff --git a/src/Response/BlockedServersCollection.php b/src/Response/BlockedServersCollection.php new file mode 100644 index 0000000..297340b --- /dev/null +++ b/src/Response/BlockedServersCollection.php @@ -0,0 +1,78 @@ +hashes = $hashes; + } + + public function offsetExists($offset): bool { + return isset($this->hashes[$offset]); + } + + public function offsetGet($offset): string { + return $this->hashes[$offset]; + } + + public function offsetSet($offset, $value): void { + $this->hashes[$offset] = $value; + } + + public function offsetUnset($offset): void { + unset($this->hashes[$offset]); + } + + public function count(): int { + return count($this->hashes); + } + + /** + * @param string $serverName + * + * @return bool + * + * @link https://wiki.vg/Mojang_API#Blocked_Servers + */ + public function isBlocked(string $serverName): bool { + if (filter_var($serverName, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + throw new InvalidArgumentException('Minecraft does not support IPv6, so this library too'); + } + + $isIp = filter_var($serverName, FILTER_VALIDATE_IP) !== false; + foreach ($this->generateSubstitutions(mb_strtolower($serverName), $isIp) as $mask) { + $hash = sha1($mask); + if (in_array($hash, $this->hashes, true)) { + return true; + } + } + + return false; + } + + private function generateSubstitutions(string $input, bool $right): iterable { + yield $input; + $parts = explode('.', $input); + while (count($parts) > 1) { + if ($right) { + array_pop($parts); + yield implode('.', $parts) . '.*'; + } else { + array_shift($parts); + yield '*.' . implode('.', $parts); + } + } + } + +} diff --git a/tests/ApiTest.php b/tests/ApiTest.php index b562a7a..f7ee0f0 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -310,6 +310,21 @@ class ApiTest extends TestCase { $this->api->playernamesToUuids($names); } + public function testBlockedServers() { + $this->mockHandler->append(new Response(200, [], trim(' + 6f2520f8bd70a718c568ab5274c56bdbbfc14ef4 + 7ea72de5f8e70a2ac45f1aa17d43f0ca3cddeedd + c005ad34245a8f2105658da2d6d6e8545ef0f0de + c645d6c6430db3069abd291ec13afebdb320714b + ') . "\n")); + + $result = $this->api->blockedServers(); + /** @var \Psr\Http\Message\RequestInterface $request */ + $request = $this->history[0]['request']; + $this->assertSame('https://sessionserver.mojang.com/blockedservers', (string)$request->getUri()); + $this->assertCount(4, $result); + } + public function testAuthenticate() { $this->mockHandler->append($this->createResponse(200, [ 'accessToken' => 'access token value', diff --git a/tests/Response/BlockedServersCollectionTest.php b/tests/Response/BlockedServersCollectionTest.php new file mode 100644 index 0000000..f830e2c --- /dev/null +++ b/tests/Response/BlockedServersCollectionTest.php @@ -0,0 +1,66 @@ +assertTrue(isset($model[0])); + $this->assertFalse(isset($model[65535])); + $this->assertSame('1', $model[0]); + $this->assertSame('2', $model[1]); + unset($model[0]); + $this->assertFalse(isset($model[0])); + $model[3] = 'find me'; + $this->assertSame('find me', $model[3]); + } + + public function testCountable() { + $model = new BlockedServersCollection(['1', '2', '3']); + $this->assertCount(3, $model); + } + + /** + * @dataProvider getIsBlockedCases + */ + public function testIsBlocked(string $serverName, bool $expectedResult) { + $model = new BlockedServersCollection([ + '6f2520f8bd70a718c568ab5274c56bdbbfc14ef4', // *.minetime.com + '48f04e89d20b15de115503f22fedfe2cb2d1ab12', // brandonisan.unusualperson.com + '4ca799b162d4ebdf2ec5e0ece2ed51fba5a3db65', // 136.243.* + 'b7a822278e90205f016c1b028122e222f836641b', // 147.117.184.134 + ]); + $this->assertSame($expectedResult, $model->isBlocked($serverName)); + } + + public function getIsBlockedCases() { + yield ['mc.minetime.com', true]; + yield ['MC.MINETIME.COM', true]; + yield ['sub.mc.minetime.com', true]; + yield ['minetime.com', false]; + yield ['minetime.mc.com', false]; + + yield ['brandonisan.unusualperson.com', true]; + yield ['other.unusualperson.com', false]; + + yield ['136.243.88.97', true]; + yield ['136.244.88.97', false]; + + yield ['147.117.184.134', true]; + yield ['147.117.184.135', false]; + } + + public function testIsBlockedWithIPv6() { + $model = new BlockedServersCollection([]); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Minecraft does not support IPv6, so this library too'); + $model->isBlocked('d860:5df:9447:61b3:d1dd:1170:146a:bcc'); + } + +}