diff --git a/.env.dist b/.env.dist index 9e0ff4e..6b2a010 100644 --- a/.env.dist +++ b/.env.dist @@ -9,8 +9,11 @@ EMAILS_RENDERER_HOST=http://emails-renderer:3000 ## Security params JWT_USER_SECRET=replace_me_for_production JWT_ENCRYPTION_KEY=thisisadummyvalue32latterslength +JWT_PRIVATE_PEM_LOCATION= +JWT_PUBLIC_PEM_LOCATION= ## External services +CHRLY_HOST=skinsystem.ely.by RECAPTCHA_PUBLIC= RECAPTCHA_SECRET= SENTRY_DSN= diff --git a/Dockerfile b/Dockerfile index 5d5fe73..56e5d20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,11 +65,39 @@ CMD ["php-fpm"] # ================================================================================ -FROM fholzer/nginx-brotli:v1.16.0 AS web +FROM fholzer/nginx-brotli:v1.19.1 AS web ENV PHP_SERVERS php:9000 -RUN rm /etc/nginx/conf.d/default.conf \ +# Add headers-more-nginx-module +RUN apk add --update --no-cache --virtual ".nginx-module-build-deps" \ + gcc \ + make \ + libc-dev \ + g++ \ + openssl-dev \ + linux-headers \ + pcre-dev \ + zlib-dev \ + libtool \ + automake \ + autoconf \ + git \ + && cd /opt \ + && git clone --depth 1 -b v0.33 --single-branch https://github.com/openresty/headers-more-nginx-module.git \ + && cd /opt/headers-more-nginx-module \ + && git submodule update --init \ + && cd /opt \ + && wget -O - http://nginx.org/download/nginx-$(nginx -v 2>&1 | sed 's@.*/@@').tar.gz | tar zxfv - \ + && cd /opt/nginx* \ + && ./configure --with-compat --add-dynamic-module=/opt/headers-more-nginx-module \ + && make modules \ + && cp /opt/nginx*/objs/ngx_http_headers_more_filter_module.so /usr/lib/nginx/modules/ \ + && sed -i '1iload_module \/usr\/lib\/nginx\/modules\/ngx_http_headers_more_filter_module.so;' /etc/nginx/nginx.conf \ + && rm -rf /opt/* \ + && apk del ".nginx-module-build-deps" \ + # Prepare image for the application + && rm /etc/nginx/conf.d/default.conf \ && mkdir -p /data/nginx/cache \ && mkdir -p /var/www/html diff --git a/api/config/config-test.php b/api/config/config-test.php index 78a3649..acce9a6 100644 --- a/api/config/config-test.php +++ b/api/config/config-test.php @@ -17,7 +17,7 @@ return [ 'authserverHost' => 'localhost', ], 'container' => [ - 'definitions' => [ + 'singletons' => [ api\components\ReCaptcha\Validator::class => function() { return new class(new GuzzleHttp\Client()) extends api\components\ReCaptcha\Validator { protected function validateValue($value) { @@ -26,7 +26,7 @@ return [ }; }, common\components\SkinsSystemApi::class => function() { - return new class extends common\components\SkinsSystemApi { + return new class('http://chrly.ely.by') extends common\components\SkinsSystemApi { public function textures(string $username): ?array { return [ 'SKIN' => [ @@ -34,6 +34,45 @@ return [ ], ]; } + + public function profile(string $username, bool $signed = false): ?array { + $account = common\models\Account::findOne(['username' => $username]); + $uuid = $account ? str_replace('-', '', $account->uuid) : '00000000000000000000000000000000'; + + $profile = [ + 'name' => $username, + 'id' => $uuid, + 'properties' => [ + [ + 'name' => 'textures', + 'value' => base64_encode(json_encode([ + 'timestamp' => Carbon\Carbon::now()->getPreciseTimestamp(3), + 'profileId' => $uuid, + 'profileName' => $username, + 'textures' => [ + 'SKIN' => [ + 'url' => 'http://ely.by/skin.png', + ], + ], + ])), + ], + [ + 'name' => 'ely', + 'value' => 'but why are you asking?', + ], + ], + ]; + + if ($signed) { + $profile['properties'][0]['signature'] = 'signature'; + } + + return $profile; + } + + public function getSignatureVerificationKey(string $format = 'pem'): string { + return "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----"; + } }; }, ], diff --git a/api/config/routes.php b/api/config/routes.php index be93a8c..61e5c3c 100644 --- a/api/config/routes.php +++ b/api/config/routes.php @@ -46,4 +46,10 @@ return [ '/mojang/profiles/' => 'mojang/api/uuid-by-username', '/mojang/profiles//names' => 'mojang/api/usernames-by-uuid', 'POST /mojang/profiles' => 'mojang/api/uuids-by-usernames', + + // authlib-injector + '/authlib-injector/authserver/' => 'authserver/authentication/', + '/authlib-injector/sessionserver/session/minecraft/join' => 'session/session/join', + '/authlib-injector/sessionserver/session/minecraft/hasJoined' => 'session/session/has-joined', + '/authlib-injector/sessionserver/session/minecraft/profile/' => 'session/session/profile', ]; diff --git a/api/controllers/AuthlibInjectorController.php b/api/controllers/AuthlibInjectorController.php new file mode 100644 index 0000000..2620dc5 --- /dev/null +++ b/api/controllers/AuthlibInjectorController.php @@ -0,0 +1,44 @@ + [ + 'class' => NginxCache::class, + 'rules' => [ + 'index' => 3600, // 1h + ], + ], + ]); + } + + public function actionIndex(SkinsSystemApi $skinsSystemApi): array { + return [ + 'meta' => [ + 'serverName' => 'Ely.by', + 'implementationName' => 'Account Ely.by adapter for the authlib-injector library', + 'implementationVersion' => '1.0.0', + 'feature.no_mojang_namespace' => true, + 'links' => [ + 'homepage' => 'https://ely.by', + 'register' => 'https://account.ely.by/register', + ], + ], + 'skinDomains' => [ + 'ely.by', + '.ely.by', + ], + 'signaturePublickey' => $skinsSystemApi->getSignatureVerificationKey(), + ]; + } + +} diff --git a/api/modules/session/controllers/SessionController.php b/api/modules/session/controllers/SessionController.php index fb1fc6c..e00b3d8 100644 --- a/api/modules/session/controllers/SessionController.php +++ b/api/modules/session/controllers/SessionController.php @@ -85,7 +85,7 @@ class SessionController extends Controller { $account = $hasJoinedForm->hasJoined(); $textures = new Textures($account); - return $textures->getMinecraftResponse(); + return $textures->getMinecraftResponse(true); } public function actionHasJoinedLegacy(): string { @@ -109,11 +109,12 @@ class SessionController extends Controller { /** * @param string $uuid + * @param string $unsigned * * @return array|null * @throws IllegalArgumentException */ - public function actionProfile(string $uuid): ?array { + public function actionProfile(string $uuid, string $unsigned = null): ?array { try { $uuid = Uuid::fromString($uuid)->toString(); } catch (\InvalidArgumentException $e) { @@ -127,7 +128,7 @@ class SessionController extends Controller { return null; } - return (new Textures($account))->getMinecraftResponse(); + return (new Textures($account))->getMinecraftResponse($unsigned === 'false'); } } diff --git a/api/tests/_pages/SessionServerRoute.php b/api/tests/_pages/SessionServerRoute.php index 677f4d5..827a895 100644 --- a/api/tests/_pages/SessionServerRoute.php +++ b/api/tests/_pages/SessionServerRoute.php @@ -19,8 +19,13 @@ class SessionServerRoute extends BasePage { $this->getActor()->sendGET('/api/minecraft/session/legacy/hasJoined', $params); } - public function profile($profileUuid) { - $this->getActor()->sendGET("/api/minecraft/session/profile/{$profileUuid}"); + public function profile(string $profileUuid, bool $signed = false) { + $url = "/api/minecraft/session/profile/{$profileUuid}"; + if ($signed) { + $url .= '?unsigned=false'; + } + + $this->getActor()->sendGET($url); } } diff --git a/api/tests/functional/_steps/SessionServerSteps.php b/api/tests/functional/_steps/SessionServerSteps.php index c57c135..adec3a0 100644 --- a/api/tests/functional/_steps/SessionServerSteps.php +++ b/api/tests/functional/_steps/SessionServerSteps.php @@ -36,19 +36,31 @@ class SessionServerSteps extends FunctionalTester { return [$username, $serverId]; } - public function canSeeValidTexturesResponse($expectedUsername, $expectedUuid) { + public function canSeeValidTexturesResponse( + string $expectedUsername, + string $expectedUuid, + bool $shouldBeSigned = false + ) { $this->seeResponseIsJson(); $this->canSeeResponseContainsJson([ 'name' => $expectedUsername, 'id' => $expectedUuid, - 'ely' => true, 'properties' => [ [ 'name' => 'textures', - 'signature' => 'Cg==', + ], + [ + 'name' => 'ely', + 'value' => 'but why are you asking?', ], ], ]); + if ($shouldBeSigned) { + $this->canSeeResponseJsonMatchesJsonPath('$.properties[?(@.name == "textures")].signature'); + } else { + $this->cantSeeResponseJsonMatchesJsonPath('$.properties[?(@.name == "textures")].signature'); + } + $this->canSeeResponseJsonMatchesJsonPath('$.properties[0].value'); $value = $this->grabDataFromResponseByJsonPath('$.properties[0].value')[0]; $decoded = json_decode(base64_decode($value), true); @@ -56,7 +68,6 @@ class SessionServerSteps extends FunctionalTester { $this->assertArrayHasKey('textures', $decoded); $this->assertSame($expectedUuid, $decoded['profileId']); $this->assertSame($expectedUsername, $decoded['profileName']); - $this->assertTrue($decoded['ely']); $textures = $decoded['textures']; $this->assertArrayHasKey('SKIN', $textures); $skinTextures = $textures['SKIN']; diff --git a/api/tests/functional/authlibInjector/HasJoinedCest.php b/api/tests/functional/authlibInjector/HasJoinedCest.php new file mode 100644 index 0000000..6653ccc --- /dev/null +++ b/api/tests/functional/authlibInjector/HasJoinedCest.php @@ -0,0 +1,52 @@ +wantTo('check hasJoined user to some server'); + [$username, $serverId] = $I->amJoined(); + + $I->sendGET('/api/authlib-injector/sessionserver/session/minecraft/hasJoined', [ + 'username' => $username, + 'serverId' => $serverId, + ]); + + $I->seeResponseCodeIs(200); + $I->canSeeValidTexturesResponse($username, 'df936908b2e1544d96f82977ec213022', true); + } + + public function wrongArguments(FunctionalTester $I) { + $I->wantTo('get error on wrong amount of arguments'); + $I->sendGET('/api/authlib-injector/sessionserver/session/minecraft/hasJoined', [ + 'wrong' => 'argument', + ]); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'IllegalArgumentException', + 'errorMessage' => 'credentials can not be null.', + ]); + } + + public function hasJoinedWithNoJoinOperation(FunctionalTester $I) { + $I->wantTo('hasJoined to some server without join call'); + $I->sendGET('/api/authlib-injector/sessionserver/session/minecraft/hasJoined', [ + 'username' => 'some-username', + 'serverId' => uuid(), + ]); + $I->seeResponseCodeIs(401); + $I->seeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'ForbiddenOperationException', + 'errorMessage' => 'Invalid token.', + ]); + } + +} diff --git a/api/tests/functional/authlibInjector/IndexCest.php b/api/tests/functional/authlibInjector/IndexCest.php new file mode 100644 index 0000000..5081bd7 --- /dev/null +++ b/api/tests/functional/authlibInjector/IndexCest.php @@ -0,0 +1,30 @@ +sendGet('/api/authlib-injector'); + $I->seeResponseCodeIs(200); + $I->canSeeResponseContainsJson([ + 'meta' => [ + 'serverName' => 'Ely.by', + 'implementationName' => 'Account Ely.by adapter for the authlib-injector library', + 'implementationVersion' => '1.0.0', + 'feature.no_mojang_namespace' => true, + 'links' => [ + 'homepage' => 'https://ely.by', + 'register' => 'https://account.ely.by/register', + ], + ], + 'skinDomains' => ['ely.by', '.ely.by'], + 'signaturePublickey' => "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----", + ]); + $I->canSeeHttpHeader('X-Accel-Expires'); + } + +} diff --git a/api/tests/functional/authlibInjector/JoinCest.php b/api/tests/functional/authlibInjector/JoinCest.php new file mode 100644 index 0000000..5ae8d8f --- /dev/null +++ b/api/tests/functional/authlibInjector/JoinCest.php @@ -0,0 +1,147 @@ +wantTo('join to server, using legacy authserver access token'); + [$accessToken] = $I->amAuthenticated(); + $I->sendPOST('/api/authlib-injector/sessionserver/session/minecraft/join', [ + 'accessToken' => $accessToken, + 'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022', + 'serverId' => uuid(), + ]); + $this->expectSuccessResponse($I); + } + + public function joinByPassJsonInPost(AuthserverSteps $I) { + $I->wantTo('join to server, passing data in body as encoded json'); + [$accessToken] = $I->amAuthenticated(); + $I->sendPOST('/api/authlib-injector/sessionserver/session/minecraft/join', [ + 'accessToken' => $accessToken, + 'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022', + 'serverId' => uuid(), + ]); + $this->expectSuccessResponse($I); + } + + /** + * @example ["df936908-b2e1-544d-96f8-2977ec213022"] + * @example ["df936908b2e1544d96f82977ec213022"] + */ + public function joinByOauth2Token(OauthSteps $I, Example $case) { + $I->wantTo('join to server, using modern oAuth2 generated token'); + $accessToken = $I->getAccessToken([P::MINECRAFT_SERVER_SESSION]); + $I->sendPOST('/api/authlib-injector/sessionserver/session/minecraft/join', [ + 'accessToken' => $accessToken, + 'selectedProfile' => $case[0], + 'serverId' => uuid(), + ]); + $this->expectSuccessResponse($I); + } + + public function joinByModernOauth2TokenWithoutPermission(OauthSteps $I) { + $I->wantTo('join to server, using moder oAuth2 generated token, but without minecraft auth permission'); + $accessToken = $I->getAccessToken(['account_info', 'account_email']); + $I->sendPOST('/api/authlib-injector/sessionserver/session/minecraft/join', [ + 'accessToken' => $accessToken, + 'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022', + 'serverId' => uuid(), + ]); + $I->seeResponseCodeIs(401); + $I->seeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'ForbiddenOperationException', + 'errorMessage' => 'The token does not have required scope.', + ]); + } + + public function joinWithExpiredToken(FunctionalTester $I) { + $I->wantTo('join to some server with expired accessToken'); + $I->sendPOST('/api/authlib-injector/sessionserver/session/minecraft/join', [ + 'accessToken' => '6042634a-a1e2-4aed-866c-c661fe4e63e2', + 'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022', + 'serverId' => uuid(), + ]); + $I->seeResponseCodeIs(401); + $I->seeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'ForbiddenOperationException', + 'errorMessage' => 'Expired access_token.', + ]); + } + + public function wrongArguments(FunctionalTester $I) { + $I->wantTo('get error on wrong amount of arguments'); + $I->sendPOST('/api/authlib-injector/sessionserver/session/minecraft/join', [ + 'wrong' => 'argument', + ]); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'IllegalArgumentException', + 'errorMessage' => 'credentials can not be null.', + ]); + } + + public function joinWithWrongAccessToken(FunctionalTester $I) { + $I->wantTo('join to some server with wrong accessToken'); + $I->sendPOST('/api/authlib-injector/sessionserver/session/minecraft/join', [ + 'accessToken' => uuid(), + 'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022', + 'serverId' => uuid(), + ]); + $I->seeResponseCodeIs(401); + $I->seeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'ForbiddenOperationException', + 'errorMessage' => 'Invalid access_token.', + ]); + } + + public function joinWithNilUuids(FunctionalTester $I) { + $I->wantTo('join to some server with nil accessToken and selectedProfile'); + $I->sendPOST('/api/authlib-injector/sessionserver/session/minecraft/join', [ + 'accessToken' => '00000000-0000-0000-0000-000000000000', + 'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022', + 'serverId' => uuid(), + ]); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'IllegalArgumentException', + 'errorMessage' => 'credentials can not be null.', + ]); + } + + public function joinByAccountMarkedForDeletion(FunctionalTester $I) { + $I->sendPOST('/api/authlib-injector/sessionserver/session/minecraft/join', [ + 'accessToken' => '239ba889-7020-4383-8d99-cd8c8aab4a2f', + 'selectedProfile' => '6383de63-8f85-4ed5-92b7-5401a1fa68cd', + 'serverId' => uuid(), + ]); + $I->canSeeResponseCodeIs(401); + $I->canSeeResponseContainsJson([ + 'error' => 'ForbiddenOperationException', + 'errorMessage' => 'Invalid credentials', + ]); + } + + private function expectSuccessResponse(FunctionalTester $I) { + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'id' => 'OK', + ]); + } + +} diff --git a/api/tests/functional/authlibInjector/ProfileCest.php b/api/tests/functional/authlibInjector/ProfileCest.php new file mode 100644 index 0000000..5144e1a --- /dev/null +++ b/api/tests/functional/authlibInjector/ProfileCest.php @@ -0,0 +1,56 @@ +sendGET("/api/authlib-injector/sessionserver/session/minecraft/profile/{$case[0]}"); + $I->canSeeValidTexturesResponse('Admin', 'df936908b2e1544d96f82977ec213022', false); + } + + public function getProfileSigned(SessionServerSteps $I) { + $I->sendGET('/api/authlib-injector/sessionserver/session/minecraft/profile/df936908b2e1544d96f82977ec213022?unsigned=false'); + $I->canSeeValidTexturesResponse('Admin', 'df936908b2e1544d96f82977ec213022', true); + } + + public function directCallWithoutUuidPart(FunctionalTester $I) { + $I->sendGET('/api/authlib-injector/sessionserver/session/minecraft/profile/'); + $I->canSeeResponseCodeIs(404); + } + + public function callWithInvalidUuid(FunctionalTester $I) { + $I->wantTo('call profile route with invalid uuid string'); + $I->sendGET('/api/authlib-injector/sessionserver/session/minecraft/profile/bla-bla-bla'); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'IllegalArgumentException', + 'errorMessage' => 'Invalid uuid format.', + ]); + } + + public function getProfileWithNonexistentUuid(FunctionalTester $I) { + $I->wantTo('get info about nonexistent uuid'); + $I->sendGET('/api/authlib-injector/sessionserver/session/minecraft/profile/' . v4()); + $I->canSeeResponseCodeIs(204); + $I->canSeeResponseEquals(''); + } + + public function getProfileOfAccountMarkedForDeletion(FunctionalTester $I) { + $I->sendGET('/api/authlib-injector/sessionserver/session/minecraft/profile/6383de63-8f85-4ed5-92b7-5401a1fa68cd'); + $I->canSeeResponseCodeIs(204); + $I->canSeeResponseEquals(''); + } + +} diff --git a/api/tests/functional/sessionserver/HasJoinedCest.php b/api/tests/functional/sessionserver/HasJoinedCest.php index 845915c..2d71c08 100644 --- a/api/tests/functional/sessionserver/HasJoinedCest.php +++ b/api/tests/functional/sessionserver/HasJoinedCest.php @@ -26,7 +26,7 @@ class HasJoinedCest { 'serverId' => $serverId, ]); $I->seeResponseCodeIs(200); - $I->canSeeValidTexturesResponse($username, 'df936908b2e1544d96f82977ec213022'); + $I->canSeeValidTexturesResponse($username, 'df936908b2e1544d96f82977ec213022', true); } public function wrongArguments(FunctionalTester $I) { diff --git a/api/tests/functional/sessionserver/ProfileCest.php b/api/tests/functional/sessionserver/ProfileCest.php index 74cda66..d9f6eb1 100644 --- a/api/tests/functional/sessionserver/ProfileCest.php +++ b/api/tests/functional/sessionserver/ProfileCest.php @@ -4,6 +4,7 @@ namespace api\tests\functional\sessionserver; use api\tests\_pages\SessionServerRoute; use api\tests\functional\_steps\SessionServerSteps; use api\tests\FunctionalTester; +use Codeception\Example; use function Ramsey\Uuid\v4; class ProfileCest { @@ -17,16 +18,20 @@ class ProfileCest { $this->route = new SessionServerRoute($I); } - public function getProfile(SessionServerSteps $I) { + /** + * @example ["df936908-b2e1-544d-96f8-2977ec213022"] + * @example ["df936908b2e1544d96f82977ec213022"] + */ + public function getProfile(SessionServerSteps $I, Example $case) { $I->wantTo('get info about player textures by uuid'); - $this->route->profile('df936908-b2e1-544d-96f8-2977ec213022'); + $this->route->profile($case[0]); $I->canSeeValidTexturesResponse('Admin', 'df936908b2e1544d96f82977ec213022'); } - public function getProfileByUuidWithoutDashes(SessionServerSteps $I) { - $I->wantTo('get info about player textures by uuid without dashes'); - $this->route->profile('df936908b2e1544d96f82977ec213022'); - $I->canSeeValidTexturesResponse('Admin', 'df936908b2e1544d96f82977ec213022'); + public function getProfileWithSignedTextures(SessionServerSteps $I) { + $I->wantTo('get info about player textures by uuid'); + $this->route->profile('df936908b2e1544d96f82977ec213022', true); + $I->canSeeValidTexturesResponse('Admin', 'df936908b2e1544d96f82977ec213022', true); } public function directCallWithoutUuidPart(FunctionalTester $I) { diff --git a/common/components/SkinsSystemApi.php b/common/components/SkinsSystemApi.php index 9caa02a..bdd4d22 100644 --- a/common/components/SkinsSystemApi.php +++ b/common/components/SkinsSystemApi.php @@ -4,15 +4,19 @@ declare(strict_types=1); namespace common\components; use GuzzleHttp\ClientInterface; +use Webmozart\Assert\Assert; use Yii; // TODO: convert to complete Chrly client library class SkinsSystemApi { - private const BASE_DOMAIN = 'http://skinsystem.ely.by'; + private string $baseDomain; - /** @var ClientInterface */ - private $client; + private ?ClientInterface $client = null; + + public function __construct(string $baseDomain) { + $this->baseDomain = $baseDomain; + } /** * @param string $username @@ -29,12 +33,47 @@ class SkinsSystemApi { return json_decode($response->getBody()->getContents(), true); } + /** + * @param string $username + * + * @return array|null + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function profile(string $username, bool $signed = false): ?array { + $url = "/profile/{$username}"; + if ($signed) { + $url .= '?unsigned=false'; + } + + $response = $this->getClient()->request('GET', $this->buildUrl($url)); + if ($response->getStatusCode() !== 200) { + return null; + } + + return json_decode($response->getBody()->getContents(), true); + } + + /** + * @param 'pem'|'der' $format + * + * @return string + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getSignatureVerificationKey(string $format = 'pem'): string { + Assert::inArray($format, ['pem', 'der']); + + return $this->getClient() + ->request('GET', $this->buildUrl("/signature-verification-key.{$format}")) + ->getBody() + ->getContents(); + } + public function setClient(ClientInterface $client): void { $this->client = $client; } private function buildUrl(string $url): string { - return self::BASE_DOMAIN . $url; + return $this->baseDomain . $url; } private function getClient(): ClientInterface { diff --git a/common/config/config-test.php b/common/config/config-test.php index ecea105..4ba070a 100644 --- a/common/config/config-test.php +++ b/common/config/config-test.php @@ -7,7 +7,7 @@ return [ ], 'components' => [ 'cache' => [ - 'class' => \yii\caching\FileCache::class, + 'class' => yii\caching\FileCache::class, ], 'security' => [ // It's allows us to increase tests speed by decreasing password hashing algorithm complexity diff --git a/common/config/config.php b/common/config/config.php index 4875b4e..2289530 100644 --- a/common/config/config.php +++ b/common/config/config.php @@ -16,10 +16,13 @@ return [ 'supportEmail' => 'support@ely.by', ], 'container' => [ - 'definitions' => [ + 'singletons' => [ GuzzleHttp\ClientInterface::class => GuzzleHttp\Client::class, Ely\Mojang\Api::class => Ely\Mojang\Api::class, - common\components\SkinsSystemApi::class => common\components\SkinsSystemApi::class, + common\components\SkinsSystemApi::class => [ + 'class' => common\components\SkinsSystemApi::class, + '__construct()' => 'http://' . (getenv('CHRLY_HOST') ?: 'skinsystem.ely.by'), + ], ], ], 'components' => [ diff --git a/common/models/Textures.php b/common/models/Textures.php index e4a83b1..39cc698 100644 --- a/common/models/Textures.php +++ b/common/models/Textures.php @@ -3,95 +3,79 @@ declare(strict_types=1); namespace common\models; -use common\components\SkinsSystemApi as SkinSystemApi; -use DateInterval; -use DateTime; +use Carbon\Carbon; +use common\components\SkinsSystemApi; +use GuzzleHttp\Client as GuzzleHttpClient; use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Exception\RequestException; use Yii; class Textures { - public $displayElyMark = true; - - /** - * @var Account - */ - protected $account; + protected Account $account; public function __construct(Account $account) { $this->account = $account; } - public function getMinecraftResponse(): array { - $response = [ - 'name' => $this->account->username, - 'id' => str_replace('-', '', $this->account->uuid), - 'properties' => [ - [ - 'name' => 'textures', - 'signature' => 'Cg==', - 'value' => $this->getTexturesValue(), + public function getMinecraftResponse(bool $signed = false): array { + $uuid = str_replace('-', '', $this->account->uuid); + $profile = $this->getProfile($signed); + if ($profile === null) { + // This case shouldn't happen at all, but until we find out how it'll actually behave, + // provide for a fallback solution + Yii::warning("By some reasons there is no profile for \"{$this->account->username}\"."); + + $profile = [ + 'name' => $this->account->username, + 'id' => $uuid, + 'properties' => [ + [ + 'name' => 'textures', + 'value' => base64_encode(json_encode([ + 'timestamp' => Carbon::now()->getPreciseTimestamp(3), + 'profileId' => $uuid, + 'profileName' => $this->account->username, + 'textures' => [], + ])), + ], + [ + 'name' => 'ely', + 'value' => 'but why are you asking?', + ], ], - ], - ]; + ]; - if ($this->displayElyMark) { - $response['ely'] = true; + if ($signed) { + // I don't remember why this value has been used, but it was, so keep the same behavior until + // figure out why it was made in this way + $profile['properties'][0]['signature'] = 'Cg=='; + } + } elseif ($profile['id'] !== $uuid) { + // Also a case that shouldn't happen, but is technically possible + Yii::warning("By an unknown reason username \"{$this->account->username}\" has an invalid id from chrly"); + $profile['id'] = $uuid; } - return $response; + return $profile; } - public function getTexturesValue($encrypted = true) { - $array = [ - 'timestamp' => (new DateTime())->add(new DateInterval('P2D'))->getTimestamp(), - 'profileId' => str_replace('-', '', $this->account->uuid), - 'profileName' => $this->account->username, - 'textures' => $this->getTextures(), - ]; - - if ($this->displayElyMark) { - $array['ely'] = true; - } - - if (!$encrypted) { - return $array; - } - - return static::encrypt($array); - } - - public function getTextures(): array { - /** @var SkinSystemApi $api */ - $api = Yii::$container->get(SkinSystemApi::class); + private function getProfile(bool $signed): ?array { + /** @var SkinsSystemApi $api */ + $api = Yii::$container->get(SkinsSystemApi::class); if (YII_ENV_PROD) { - $api->setClient(new \GuzzleHttp\Client([ + $api->setClient(new GuzzleHttpClient([ 'connect_timeout' => 2, - 'decode_content' => false, - 'read_timeout' => 5, - 'stream' => true, - 'timeout' => 5, + 'read_timeout' => 7, ])); } try { - $textures = $api->textures($this->account->username); - } catch (RequestException $e) { - Yii::warning('Cannot get textures from skinsystem.ely.by. Exception message is ' . $e->getMessage()); + return $api->profile($this->account->username, $signed); } catch (GuzzleException $e) { - Yii::warning($e); + Yii::warning('Cannot get textures from skinsystem.ely.by. Exception message is ' . $e->getMessage()); } - return $textures ?? []; - } - - public static function encrypt(array $data): string { - return base64_encode(stripcslashes(json_encode($data))); - } - - public static function decrypt($string, $assoc = true) { - return json_decode(base64_decode($string), $assoc); + return null; } } diff --git a/docker/nginx/account.ely.by.conf.template b/docker/nginx/account.ely.by.conf.template index 24dd49c..6b5e841 100644 --- a/docker/nginx/account.ely.by.conf.template +++ b/docker/nginx/account.ely.by.conf.template @@ -35,6 +35,10 @@ server { } location / { + if ($request_uri = '/') { + more_set_headers "X-Authlib-Injector-API-Location: /api/authlib-injector"; + } + root $frontend_path; access_log off; etag on; diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf index 811236a..c14d494 100644 --- a/docker/nginx/nginx.conf +++ b/docker/nginx/nginx.conf @@ -1,3 +1,5 @@ +load_module /usr/lib/nginx/modules/ngx_http_headers_more_filter_module.so; + user nginx; worker_processes auto; diff --git a/docker/php/docker-entrypoint.sh b/docker/php/docker-entrypoint.sh index 220b9ae..40b06a0 100755 --- a/docker/php/docker-entrypoint.sh +++ b/docker/php/docker-entrypoint.sh @@ -37,8 +37,9 @@ done # Fix permissions for cron tasks chmod 644 /etc/cron.d/* -JWT_PRIVATE_PEM_LOCATION="/var/www/html/data/certs/private.pem" -JWT_PUBLIC_PEM_LOCATION="/var/www/html/data/certs/public.pem" +JWT_PRIVATE_PEM_LOCATION="${JWT_PRIVATE_PEM_LOCATION:-/var/www/html/data/certs/private.pem}" +JWT_PUBLIC_PEM_LOCATION="${JWT_PUBLIC_PEM_LOCATION:-/var/www/html/data/certs/public.pem}" + if [ ! -f "$JWT_PRIVATE_PEM_LOCATION" ] ; then echo "There is no private key. Generating the new one." openssl ecparam -name prime256v1 -genkey -noout -out "$JWT_PRIVATE_PEM_LOCATION"