mirror of
				https://github.com/elyby/accounts.git
				synced 2025-05-31 14:11:46 +05:30 
			
		
		
		
	Merge branch 'authlib_injector'
This commit is contained in:
		| @@ -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= | ||||
|   | ||||
							
								
								
									
										32
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								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 | ||||
|  | ||||
|   | ||||
| @@ -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-----"; | ||||
|                     } | ||||
|                 }; | ||||
|             }, | ||||
|         ], | ||||
|   | ||||
| @@ -46,4 +46,10 @@ return [ | ||||
|     '/mojang/profiles/<username>' => 'mojang/api/uuid-by-username', | ||||
|     '/mojang/profiles/<uuid>/names' => 'mojang/api/usernames-by-uuid', | ||||
|     'POST /mojang/profiles' => 'mojang/api/uuids-by-usernames', | ||||
|  | ||||
|     // authlib-injector | ||||
|     '/authlib-injector/authserver/<action>' => 'authserver/authentication/<action>', | ||||
|     '/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/<uuid>' => 'session/session/profile', | ||||
| ]; | ||||
|   | ||||
							
								
								
									
										44
									
								
								api/controllers/AuthlibInjectorController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								api/controllers/AuthlibInjectorController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <?php | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace api\controllers; | ||||
|  | ||||
| use api\filters\NginxCache; | ||||
| use common\components\SkinsSystemApi; | ||||
| use yii\helpers\ArrayHelper; | ||||
| use yii\web\Controller as BaseController; | ||||
|  | ||||
| final class AuthlibInjectorController extends BaseController { | ||||
|  | ||||
|     public function behaviors(): array { | ||||
|         return ArrayHelper::merge(parent::behaviors(), [ | ||||
|             'nginxCache' => [ | ||||
|                 '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(), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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'); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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']; | ||||
|   | ||||
							
								
								
									
										52
									
								
								api/tests/functional/authlibInjector/HasJoinedCest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								api/tests/functional/authlibInjector/HasJoinedCest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| <?php | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace api\tests\functional\authlibInjector; | ||||
|  | ||||
| use api\tests\functional\_steps\SessionServerSteps; | ||||
| use api\tests\FunctionalTester; | ||||
| use function Ramsey\Uuid\v4 as uuid; | ||||
|  | ||||
| class HasJoinedCest { | ||||
|  | ||||
|     public function hasJoined(SessionServerSteps $I) { | ||||
|         $I->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.', | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										30
									
								
								api/tests/functional/authlibInjector/IndexCest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								api/tests/functional/authlibInjector/IndexCest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <?php | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace api\tests\functional\authlibInjector; | ||||
|  | ||||
| use api\tests\FunctionalTester; | ||||
|  | ||||
| class IndexCest { | ||||
|  | ||||
|     public function index(FunctionalTester $I) { | ||||
|         $I->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'); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										147
									
								
								api/tests/functional/authlibInjector/JoinCest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								api/tests/functional/authlibInjector/JoinCest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| <?php | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace api\tests\functional\authlibInjector; | ||||
|  | ||||
| use api\rbac\Permissions as P; | ||||
| use api\tests\functional\_steps\AuthserverSteps; | ||||
| use api\tests\functional\_steps\OauthSteps; | ||||
| use api\tests\FunctionalTester; | ||||
| use Codeception\Example; | ||||
| use function Ramsey\Uuid\v4 as uuid; | ||||
|  | ||||
| class JoinCest { | ||||
|  | ||||
|     public function joinByLegacyAuthserver(AuthserverSteps $I) { | ||||
|         $I->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', | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										56
									
								
								api/tests/functional/authlibInjector/ProfileCest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								api/tests/functional/authlibInjector/ProfileCest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| <?php | ||||
| declare(strict_types=1); | ||||
|  | ||||
| namespace api\tests\functional\authlibInjector; | ||||
|  | ||||
| use api\tests\functional\_steps\SessionServerSteps; | ||||
| use api\tests\FunctionalTester; | ||||
| use Codeception\Example; | ||||
| use function Ramsey\Uuid\v4; | ||||
|  | ||||
| class ProfileCest { | ||||
|  | ||||
|     /** | ||||
|      * @example ["df936908-b2e1-544d-96f8-2977ec213022"] | ||||
|      * @example ["df936908b2e1544d96f82977ec213022"] | ||||
|      */ | ||||
|     public function getProfile(SessionServerSteps $I, Example $case) { | ||||
|         $I->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(''); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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' => [ | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| load_module /usr/lib/nginx/modules/ngx_http_headers_more_filter_module.so; | ||||
|  | ||||
| user nginx; | ||||
| worker_processes auto; | ||||
|  | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user