diff --git a/api/modules/session/controllers/SessionController.php b/api/modules/session/controllers/SessionController.php index 1784917..583b5a0 100644 --- a/api/modules/session/controllers/SessionController.php +++ b/api/modules/session/controllers/SessionController.php @@ -4,9 +4,12 @@ namespace api\modules\session\controllers; use api\controllers\ApiController; use api\modules\session\exceptions\ForbiddenOperationException; use api\modules\session\exceptions\SessionServerException; +use api\modules\session\models\HasJoinedForm; use api\modules\session\models\JoinForm; use api\modules\session\models\protocols\LegacyJoin; +use api\modules\session\models\protocols\ModernHasJoined; use api\modules\session\models\protocols\ModernJoin; +use common\models\Textures; use Yii; use yii\web\Response; @@ -57,4 +60,38 @@ class SessionController extends ApiController { return 'OK'; } + public function actionHasJoined() { + Yii::$app->response->format = Response::FORMAT_JSON; + + $data = Yii::$app->request->get(); + $protocol = new ModernHasJoined($data['username'] ?? '', $data['serverId'] ?? ''); + $hasJoinedForm = new HasJoinedForm($protocol); + $account = $hasJoinedForm->hasJoined(); + $textures = new Textures($account); + + return $textures->getMinecraftResponse(); + } + + public function actionHasJoinedLegacy() { + Yii::$app->response->format = Response::FORMAT_RAW; + + $data = Yii::$app->request->get(); + $protocol = new ModernHasJoined($data['user'] ?? '', $data['serverId'] ?? ''); + $hasJoinedForm = new HasJoinedForm($protocol); + try { + $hasJoinedForm->hasJoined(); + } catch (SessionServerException $e) { + Yii::$app->response->statusCode = $e->statusCode; + if ($e instanceof ForbiddenOperationException) { + $message = 'NO'; + } else { + $message = $e->getMessage(); + } + + return $message; + } + + return 'YES'; + } + } diff --git a/api/modules/session/models/HasJoinedForm.php b/api/modules/session/models/HasJoinedForm.php new file mode 100644 index 0000000..622ce71 --- /dev/null +++ b/api/modules/session/models/HasJoinedForm.php @@ -0,0 +1,52 @@ +protocol = $protocol; + parent::__construct($config); + } + + public function hasJoined() : Account { + if (!$this->protocol->validate()) { + throw new IllegalArgumentException(); + } + + $serverId = $this->protocol->getServerId(); + $username = $this->protocol->getUsername(); + + Session::info( + "Server with server_id = '{$serverId}' trying to verify has joined user with username = '{$username}'." + ); + + $joinModel = SessionModel::find($username, $serverId); + if ($joinModel === null) { + Session::error("Not found join operation for username = '{$username}'."); + throw new ForbiddenOperationException('Invalid token.'); + } + + $joinModel->delete(); + $account = $joinModel->getAccount(); + if ($account === null) { + throw new ErrorException('Account must exists'); + } + + Session::info( + "User with username = '{$username}' successfully verified by server with server_id = '{$serverId}'." + ); + + return $account; + } + +} diff --git a/api/modules/session/models/JoinForm.php b/api/modules/session/models/JoinForm.php index d7e9e17..e0ac1de 100644 --- a/api/modules/session/models/JoinForm.php +++ b/api/modules/session/models/JoinForm.php @@ -18,9 +18,9 @@ use yii\web\UnauthorizedHttpException; class JoinForm extends Model { - private $accessToken; - private $selectedProfile; - private $serverId; + public $accessToken; + public $selectedProfile; + public $serverId; /** * @var Account|null diff --git a/api/modules/session/models/SessionModel.php b/api/modules/session/models/SessionModel.php index 1efd748..da43391 100644 --- a/api/modules/session/models/SessionModel.php +++ b/api/modules/session/models/SessionModel.php @@ -1,6 +1,7 @@ redis->executeCommand('DEL', [static::buildKey($this->username, $this->serverId)]); } - protected static function buildKey($username, $serverId) { + /** + * @return Account|null + * TODO: после перехода на PHP 7.1 установить тип как ?Account + */ + public function getAccount() { + return Account::findOne(['username' => $this->username]); + } + + protected static function buildKey($username, $serverId) : string { return md5('minecraft:join-server:' . mb_strtolower($username) . ':' . $serverId); } diff --git a/api/modules/session/models/protocols/BaseHasJoined.php b/api/modules/session/models/protocols/BaseHasJoined.php new file mode 100644 index 0000000..b10ce49 --- /dev/null +++ b/api/modules/session/models/protocols/BaseHasJoined.php @@ -0,0 +1,31 @@ +username = $username; + $this->serverId = $serverId; + } + + public function getUsername() : string { + return $this->username; + } + + public function getServerId() : string { + return $this->serverId; + } + + public function validate() : bool { + $validator = new RequiredValidator(); + + return $validator->validate($this->username) + && $validator->validate($this->serverId); + } + +} diff --git a/api/modules/session/models/protocols/HasJoinedInterface.php b/api/modules/session/models/protocols/HasJoinedInterface.php new file mode 100644 index 0000000..96a051a --- /dev/null +++ b/api/modules/session/models/protocols/HasJoinedInterface.php @@ -0,0 +1,12 @@ +getClient()->get($this->getBuildUrl('/textures/' . $username)); + $textures = json_decode($response->getBody(), true); + + return $textures; + } + + protected function getBuildUrl(string $url) : string { + return self::BASE_DOMAIN . $url; + } + + /** + * @return GuzzleClient + */ + protected function getClient() : GuzzleClient { + return Yii::$app->guzzle; + } + +} diff --git a/common/config/main.php b/common/config/main.php index 92eb468..6ef7bfe 100644 --- a/common/config/main.php +++ b/common/config/main.php @@ -23,6 +23,9 @@ return [ 'amqp' => [ 'class' => \common\components\RabbitMQ\Component::class, ], + 'guzzle' => [ + 'class' => \GuzzleHttp\Client::class, + ], ], 'aliases' => [ '@bower' => '@vendor/bower-asset', diff --git a/common/models/Textures.php b/common/models/Textures.php new file mode 100644 index 0000000..761e227 --- /dev/null +++ b/common/models/Textures.php @@ -0,0 +1,71 @@ +account = $account; + } + + public function getMinecraftResponse() { + $response = [ + 'name' => $this->account->username, + 'id' => str_replace('-', '', $this->account->uuid), + 'properties' => [ + [ + 'name' => 'textures', + 'signature' => 'Cg==', + 'value' => $this->getTexturesValue(), + ], + ], + ]; + + if ($this->displayElyMark) { + $response['ely'] = true; + } + + return $response; + } + + public function getTexturesValue($encrypted = true) { + $array = [ + 'timestamp' => time() + 60 * 60 * 24 * 2, + 'profileId' => str_replace('-', '', $this->account->uuid), + 'profileName' => $this->account->username, + 'textures' => $this->getTextures(), + ]; + + if ($this->displayElyMark) { + $array['ely'] = true; + } + + if (!$encrypted) { + return $array; + } else { + return $this->encrypt($array); + } + } + + public function getTextures() { + $api = new SkinSystemApi(); + return $api->textures($this->account->username); + } + + public static function encrypt(array $data) { + return base64_encode(stripcslashes(json_encode($data))); + } + + public static function decrypt($string, $assoc = true) { + return json_decode(base64_decode($string), $assoc); + } + +} diff --git a/tests/codeception/api/_pages/SessionServerRoute.php b/tests/codeception/api/_pages/SessionServerRoute.php index e40c3fd..d136aac 100644 --- a/tests/codeception/api/_pages/SessionServerRoute.php +++ b/tests/codeception/api/_pages/SessionServerRoute.php @@ -18,4 +18,14 @@ class SessionServerRoute extends BasePage { $this->actor->sendGET($this->getUrl(), $params); } + public function hasJoined(array $params) { + $this->route = ['sessionserver/session/has-joined']; + $this->actor->sendGET($this->getUrl(), $params); + } + + public function hasJoinedLegacy(array $params) { + $this->route = ['sessionserver/session/has-joined-legacy']; + $this->actor->sendGET($this->getUrl(), $params); + } + } diff --git a/tests/codeception/api/functional.suite.yml b/tests/codeception/api/functional.suite.yml index 49ff5d6..4669215 100644 --- a/tests/codeception/api/functional.suite.yml +++ b/tests/codeception/api/functional.suite.yml @@ -6,6 +6,7 @@ modules: - tests\codeception\common\_support\FixtureHelper - Redis - AMQP + - Asserts - REST: depends: Yii2 config: diff --git a/tests/codeception/api/functional/_steps/SessionServerSteps.php b/tests/codeception/api/functional/_steps/SessionServerSteps.php new file mode 100644 index 0000000..6bcb3fe --- /dev/null +++ b/tests/codeception/api/functional/_steps/SessionServerSteps.php @@ -0,0 +1,38 @@ +scenario); + $accessToken = $oauthSteps->getAccessToken([S::MINECRAFT_SERVER_SESSION]); + $route = new SessionServerRoute($this); + $serverId = Uuid::uuid(); + $username = 'Admin'; + + if ($byLegacy) { + $route->joinLegacy([ + 'sessionId' => 'token:' . $accessToken . ':' . 'df936908-b2e1-544d-96f8-2977ec213022', + 'user' => $username, + 'serverId' => $serverId, + ]); + + $this->canSeeResponseEquals('OK'); + } else { + $route->join([ + 'accessToken' => $accessToken, + 'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022', + 'serverId' => $serverId, + ]); + + $this->canSeeResponseContainsJson(['id' => 'OK']); + } + + return [$username, $serverId]; + } + +} diff --git a/tests/codeception/api/functional/sessionserver/HasJoinedCest.php b/tests/codeception/api/functional/sessionserver/HasJoinedCest.php new file mode 100644 index 0000000..682099a --- /dev/null +++ b/tests/codeception/api/functional/sessionserver/HasJoinedCest.php @@ -0,0 +1,83 @@ +route = new SessionServerRoute($I); + } + + public function hasJoined(SessionServerSteps $I) { + $I->wantTo('check hasJoined user to some server'); + list($username, $serverId) = $I->amJoined(); + + $this->route->hasJoined([ + 'username' => $username, + 'serverId' => $serverId, + ]); + $I->seeResponseCodeIs(200); + $I->seeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'name' => $username, + 'id' => 'df936908b2e1544d96f82977ec213022', + 'ely' => true, + 'properties' => [ + [ + 'name' => 'textures', + 'signature' => 'Cg==', + ], + ], + ]); + $I->canSeeResponseJsonMatchesJsonPath('$.properties[0].value'); + $value = json_decode($I->grabResponse(), true)['properties'][0]['value']; + $decoded = json_decode(base64_decode($value), true); + $I->assertArrayHasKey('timestamp', $decoded); + $I->assertArrayHasKey('textures', $decoded); + $I->assertEquals('df936908b2e1544d96f82977ec213022', $decoded['profileId']); + $I->assertEquals('Admin', $decoded['profileName']); + $I->assertTrue($decoded['ely']); + $textures = $decoded['textures']; + $I->assertArrayHasKey('SKIN', $textures); + $skinTextures = $textures['SKIN']; + $I->assertArrayHasKey('url', $skinTextures); + $I->assertArrayHasKey('hash', $skinTextures); + } + + public function wrongArguments(FunctionalTester $I) { + $I->wantTo('get error on wrong amount of arguments'); + $this->route->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'); + $this->route->hasJoined([ + 'username' => 'some-username', + 'serverId' => Uuid::uuid(), + ]); + $I->seeResponseCodeIs(401); + $I->seeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'ForbiddenOperationException', + 'errorMessage' => 'Invalid token.', + ]); + } + +} diff --git a/tests/codeception/api/functional/sessionserver/HasJoinedLegacyCest.php b/tests/codeception/api/functional/sessionserver/HasJoinedLegacyCest.php new file mode 100644 index 0000000..b0133ac --- /dev/null +++ b/tests/codeception/api/functional/sessionserver/HasJoinedLegacyCest.php @@ -0,0 +1,51 @@ +route = new SessionServerRoute($I); + } + + public function hasJoined(SessionServerSteps $I) { + $I->wantTo('test hasJoined user to some server by legacy version'); + list($username, $serverId) = $I->amJoined(true); + + $this->route->hasJoinedLegacy([ + 'user' => $username, + 'serverId' => $serverId, + ]); + $I->seeResponseCodeIs(200); + $I->canSeeResponseEquals('YES'); + } + + public function wrongArguments(FunctionalTester $I) { + $I->wantTo('get error on wrong amount of arguments'); + $this->route->hasJoinedLegacy([ + 'wrong' => 'argument', + ]); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseEquals('credentials can not be null.'); + } + + public function hasJoinedWithNoJoinOperation(FunctionalTester $I) { + $I->wantTo('hasJoined by legacy version to some server without join call'); + $this->route->hasJoinedLegacy([ + 'user' => 'random-username', + 'serverId' => Uuid::uuid(), + ]); + $I->seeResponseCodeIs(401); + $I->canSeeResponseEquals('NO'); + } + +}