mirror of
				https://github.com/elyby/accounts.git
				synced 2025-05-31 14:11:46 +05:30 
			
		
		
		
	Resolves #2. Implemented authlib-injector support
This commit is contained in:
		@@ -9,6 +9,8 @@ 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
 | 
			
		||||
RECAPTCHA_PUBLIC=
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
@@ -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,6 +4,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace common\components;
 | 
			
		||||
 | 
			
		||||
use GuzzleHttp\ClientInterface;
 | 
			
		||||
use Webmozart\Assert\Assert;
 | 
			
		||||
use Yii;
 | 
			
		||||
 | 
			
		||||
// TODO: convert to complete Chrly client library
 | 
			
		||||
@@ -11,8 +12,7 @@ class SkinsSystemApi {
 | 
			
		||||
 | 
			
		||||
    private const BASE_DOMAIN = 'http://skinsystem.ely.by';
 | 
			
		||||
 | 
			
		||||
    /** @var ClientInterface */
 | 
			
		||||
    private $client;
 | 
			
		||||
    private ?ClientInterface $client = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param string $username
 | 
			
		||||
@@ -29,12 +29,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 static::BASE_DOMAIN . $url;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getClient(): ClientInterface {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ 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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = [
 | 
			
		||||
    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' => str_replace('-', '', $this->account->uuid),
 | 
			
		||||
                'id' => $uuid,
 | 
			
		||||
                'properties' => [
 | 
			
		||||
                    [
 | 
			
		||||
                        'name' => 'textures',
 | 
			
		||||
                    'signature' => 'Cg==',
 | 
			
		||||
                    'value' => $this->getTexturesValue(),
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if ($this->displayElyMark) {
 | 
			
		||||
            $response['ely'] = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $response;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTexturesValue($encrypted = true) {
 | 
			
		||||
        $array = [
 | 
			
		||||
            'timestamp' => (new DateTime())->add(new DateInterval('P2D'))->getTimestamp(),
 | 
			
		||||
            'profileId' => str_replace('-', '', $this->account->uuid),
 | 
			
		||||
                        'value' => base64_encode(json_encode([
 | 
			
		||||
                            'timestamp' => Carbon::now()->getPreciseTimestamp(3),
 | 
			
		||||
                            'profileId' => $uuid,
 | 
			
		||||
                            'profileName' => $this->account->username,
 | 
			
		||||
            'textures' => $this->getTextures(),
 | 
			
		||||
                            'textures' => [],
 | 
			
		||||
                        ])),
 | 
			
		||||
                    ],
 | 
			
		||||
                    [
 | 
			
		||||
                        'name' => 'ely',
 | 
			
		||||
                        'value' => 'but why are you asking?',
 | 
			
		||||
                    ],
 | 
			
		||||
                ],
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
        if ($this->displayElyMark) {
 | 
			
		||||
            $array['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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!$encrypted) {
 | 
			
		||||
            return $array;
 | 
			
		||||
        return $profile;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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