From e8b5e90a9152bd9c053c625f1478c157de8533da Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Mon, 5 Sep 2016 17:55:38 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=B5=D0=BC=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B3=D0=B0=20Join=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=83=D1=87=D1=91=D1=82=D0=B0=20Legacy=20API=20?= =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B0=20=D1=87=D1=82?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= =?UTF-8?q?=20=D0=B8=D0=B7=20POST=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=B0,=20=D0=B5=D1=81=D0=BB=D0=B8=20=D0=BE=D0=BD=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D0=BD=D1=8B=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BA=20RAW=20json=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20StringHelper::isUuid()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/controllers/SessionController.php | 36 +++++++- api/modules/session/models/Form.php | 27 ------ api/modules/session/models/JoinForm.php | 68 +++++++++++--- .../session/models/protocols/BaseJoin.php | 14 +++ .../models/protocols/JoinInterface.php | 15 +++ .../session/models/protocols/LegacyJoin.php | 63 +++++++++++++ .../session/models/protocols/ModernJoin.php | 38 ++++++++ common/helpers/StringHelper.php | 12 ++- .../api/_pages/SessionServerRoute.php | 5 + .../api/functional/sessionserver/JoinCest.php | 15 ++- .../sessionserver/JoinLegacyCest.php | 92 +++++++++++++++++++ .../common/unit/helpers/StringHelperTest.php | 2 +- 12 files changed, 338 insertions(+), 49 deletions(-) delete mode 100644 api/modules/session/models/Form.php create mode 100644 api/modules/session/models/protocols/BaseJoin.php create mode 100644 api/modules/session/models/protocols/JoinInterface.php create mode 100644 api/modules/session/models/protocols/LegacyJoin.php create mode 100644 api/modules/session/models/protocols/ModernJoin.php create mode 100644 tests/codeception/api/functional/sessionserver/JoinLegacyCest.php diff --git a/api/modules/session/controllers/SessionController.php b/api/modules/session/controllers/SessionController.php index 466a051..1784917 100644 --- a/api/modules/session/controllers/SessionController.php +++ b/api/modules/session/controllers/SessionController.php @@ -2,7 +2,13 @@ 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\JoinForm; +use api\modules\session\models\protocols\LegacyJoin; +use api\modules\session\models\protocols\ModernJoin; +use Yii; +use yii\web\Response; class SessionController extends ApiController { @@ -14,15 +20,41 @@ class SessionController extends ApiController { } public function actionJoin() { - $joinForm = new JoinForm(); - $joinForm->loadByPost(); + Yii::$app->response->format = Response::FORMAT_JSON; + + $data = Yii::$app->request->post(); + if (empty($data)) { + // TODO: помнится у Yii2 есть механизм парсинга данных входящего запроса. Лучше будет сделать это там + $data = json_decode(Yii::$app->request->getRawBody(), true); + } + + $protocol = new ModernJoin($data['accessToken'] ?? '', $data['selectedProfile'] ?? '', $data['serverId'] ?? ''); + $joinForm = new JoinForm($protocol); $joinForm->join(); return ['id' => 'OK']; } public function actionJoinLegacy() { + Yii::$app->response->format = Response::FORMAT_RAW; + $data = Yii::$app->request->get(); + $protocol = new LegacyJoin($data['user'] ?? '', $data['sessionId'] ?? '', $data['serverId'] ?? ''); + $joinForm = new JoinForm($protocol); + try { + $joinForm->join(); + } catch (SessionServerException $e) { + Yii::$app->response->statusCode = $e->statusCode; + if ($e instanceof ForbiddenOperationException) { + $message = 'Ely.by authorization required'; + } else { + $message = $e->getMessage(); + } + + return $message; + } + + return 'OK'; } } diff --git a/api/modules/session/models/Form.php b/api/modules/session/models/Form.php deleted file mode 100644 index bd4a383..0000000 --- a/api/modules/session/models/Form.php +++ /dev/null @@ -1,27 +0,0 @@ -load(Yii::$app->request->get()); - } - - public function loadByPost() { - $data = Yii::$app->request->post(); - // TODO: проверить, парсит ли Yii2 raw body и что он делает, если там неспаршенный json - /*if (empty($data)) { - $data = $request->getJsonRawBody(true); - }*/ - - return $this->load($data); - } - -} diff --git a/api/modules/session/models/JoinForm.php b/api/modules/session/models/JoinForm.php index dbd6d1a..d7e9e17 100644 --- a/api/modules/session/models/JoinForm.php +++ b/api/modules/session/models/JoinForm.php @@ -3,60 +3,92 @@ namespace api\modules\session\models; use api\modules\session\exceptions\ForbiddenOperationException; use api\modules\session\exceptions\IllegalArgumentException; +use api\modules\session\models\protocols\JoinInterface; use api\modules\session\Module as Session; use api\modules\session\validators\RequiredValidator; +use common\helpers\StringHelper; use common\models\OauthScope as S; use common\validators\UuidValidator; use common\models\Account; use common\models\MinecraftAccessKey; use Yii; use yii\base\ErrorException; +use yii\base\Model; use yii\web\UnauthorizedHttpException; -class JoinForm extends Form { +class JoinForm extends Model { - public $accessToken; - public $selectedProfile; - public $serverId; + private $accessToken; + private $selectedProfile; + private $serverId; + /** + * @var Account|null + */ private $account; + /** + * @var JoinInterface + */ + private $protocol; + + public function __construct(JoinInterface $protocol, array $config = []) { + $this->protocol = $protocol; + $this->accessToken = $protocol->getAccessToken(); + $this->selectedProfile = $protocol->getSelectedProfile(); + $this->serverId = $protocol->getServerId(); + + parent::__construct($config); + } + public function rules() { return [ - [['accessToken', 'selectedProfile', 'serverId'], RequiredValidator::class], + [['accessToken', 'serverId'], RequiredValidator::class], [['accessToken', 'selectedProfile'], 'validateUuid'], [['accessToken'], 'validateAccessToken'], ]; } public function join() { - Session::info( - "User with access_token = '{$this->accessToken}' trying join to server with server_id = " . - "'{$this->serverId}'." - ); + $serverId = $this->serverId; + $accessToken = $this->accessToken; + Session::info("User with access_token = '{$accessToken}' trying join to server with server_id = '{$serverId}'."); if (!$this->validate()) { return false; } $account = $this->getAccount(); - $sessionModel = new SessionModel($account->username, $this->serverId); + $sessionModel = new SessionModel($account->username, $serverId); if (!$sessionModel->save()) { throw new ErrorException('Cannot save join session model'); } Session::info( - "User with access_token = '{$this->accessToken}' and nickname = '{$account->username}' successfully " . - "joined to server_id = '{$this->serverId}'." + "User with access_token = '{$accessToken}' and nickname = '{$account->username}' successfully joined to " . + "server_id = '{$serverId}'." ); return true; } + public function validate($attributeNames = null, $clearErrors = true) { + if (!$this->protocol->validate()) { + throw new IllegalArgumentException(); + } + + return parent::validate($attributeNames, $clearErrors); + } + public function validateUuid($attribute) { if ($this->hasErrors($attribute)) { return; } + if ($attribute === 'selectedProfile' && !StringHelper::isUuid($this->selectedProfile)) { + // Это нормально. Там может быть ник игрока, если это Legacy авторизация + return; + } + $validator = new UuidValidator(); $validator->validateAttribute($this, $attribute); @@ -101,12 +133,20 @@ class JoinForm extends Form { throw new ForbiddenOperationException('Expired access_token.'); } - if ($account->uuid !== $this->selectedProfile) { + $selectedProfile = $this->selectedProfile; + $isUuid = StringHelper::isUuid($selectedProfile); + if ($isUuid && $account->uuid !== $selectedProfile) { Session::error( - "User with access_token = '{$accessToken}' trying to join with identity = '{$this->selectedProfile}'," . + "User with access_token = '{$accessToken}' trying to join with identity = '{$selectedProfile}'," . " but access_token issued to account with id = '{$account->uuid}'." ); throw new ForbiddenOperationException('Wrong selected_profile.'); + } elseif (!$isUuid && $account->username !== $selectedProfile) { + Session::error( + "User with access_token = '{$accessToken}' trying to join with identity = '{$selectedProfile}'," . + " but access_token issued to account with username = '{$account->username}'." + ); + throw new ForbiddenOperationException('Invalid credentials'); } $this->account = $account; diff --git a/api/modules/session/models/protocols/BaseJoin.php b/api/modules/session/models/protocols/BaseJoin.php new file mode 100644 index 0000000..d025e83 --- /dev/null +++ b/api/modules/session/models/protocols/BaseJoin.php @@ -0,0 +1,14 @@ +user = $user; + $this->sessionId = $sessionId; + $this->serverId = $serverId; + + $this->parseSessionId($this->sessionId); + } + + public function getAccessToken() : string { + return $this->accessToken; + } + + public function getSelectedProfile() : string { + return $this->uuid ?: $this->user; + } + + public function getServerId() : string { + return $this->serverId; + } + + /** + * @return bool + */ + public function validate() : bool { + $validator = new RequiredValidator(); + + return $validator->validate($this->accessToken) + && $validator->validate($this->user) + && $validator->validate($this->serverId); + } + + /** + * Метод проводит инициализацию значений полей для соотвествия общим канонам + * именования в проекте + * + * Бьём по ':' для учёта авторизации в современных лаунчерах и входе на более старую + * версию игры. Там sessionId передаётся как "token:{accessToken}:{uuid}", так что это нужно обработать + */ + protected function parseSessionId(string $sessionId) { + $parts = explode(':', $sessionId); + if (count($parts) === 3) { + $this->accessToken = $parts[1]; + $this->uuid = $parts[2]; + } else { + $this->accessToken = $this->sessionId; + } + } + +} diff --git a/api/modules/session/models/protocols/ModernJoin.php b/api/modules/session/models/protocols/ModernJoin.php new file mode 100644 index 0000000..f535ce5 --- /dev/null +++ b/api/modules/session/models/protocols/ModernJoin.php @@ -0,0 +1,38 @@ +accessToken = $accessToken; + $this->selectedProfile = $selectedProfile; + $this->serverId = $serverId; + } + + public function getAccessToken() : string { + return $this->accessToken; + } + + public function getSelectedProfile() : string { + return $this->selectedProfile; + } + + public function getServerId() : string { + return $this->serverId; + } + + public function validate() : bool { + $validator = new RequiredValidator(); + + return $validator->validate($this->accessToken) + && $validator->validate($this->selectedProfile) + && $validator->validate($this->serverId); + } + +} diff --git a/common/helpers/StringHelper.php b/common/helpers/StringHelper.php index 5e4c300..efa49d9 100644 --- a/common/helpers/StringHelper.php +++ b/common/helpers/StringHelper.php @@ -1,6 +1,8 @@ actor->sendPOST($this->getUrl(), $params); } + public function joinLegacy(array $params) { + $this->route = ['sessionserver/session/join-legacy']; + $this->actor->sendGET($this->getUrl(), $params); + } + } diff --git a/tests/codeception/api/functional/sessionserver/JoinCest.php b/tests/codeception/api/functional/sessionserver/JoinCest.php index ce7709a..00d9cf3 100644 --- a/tests/codeception/api/functional/sessionserver/JoinCest.php +++ b/tests/codeception/api/functional/sessionserver/JoinCest.php @@ -30,8 +30,19 @@ class JoinCest { $this->expectSuccessResponse($I); } - public function joinByModernOauth2Token(OauthSteps $I) { - $I->wantTo('join to server, using moder oAuth2 generated token'); + public function joinByPassJsonInPost(AuthserverSteps $I) { + $I->wantTo('join to server, passing data in body as encoded json'); + list($accessToken) = $I->amAuthenticated(); + $this->route->join(json_encode([ + 'accessToken' => $accessToken, + 'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022', + 'serverId' => Uuid::uuid(), + ])); + $this->expectSuccessResponse($I); + } + + public function joinByOauth2Token(OauthSteps $I) { + $I->wantTo('join to server, using modern oAuth2 generated token'); $accessToken = $I->getAccessToken([S::MINECRAFT_SERVER_SESSION]); $this->route->join([ 'accessToken' => $accessToken, diff --git a/tests/codeception/api/functional/sessionserver/JoinLegacyCest.php b/tests/codeception/api/functional/sessionserver/JoinLegacyCest.php new file mode 100644 index 0000000..7818870 --- /dev/null +++ b/tests/codeception/api/functional/sessionserver/JoinLegacyCest.php @@ -0,0 +1,92 @@ +route = new SessionServerRoute($I); + } + + public function joinByLegacyAuthserver(AuthserverSteps $I) { + $I->wantTo('join to server by legacy protocol, using legacy authserver access token'); + list($accessToken) = $I->amAuthenticated(); + $this->route->joinLegacy([ + 'sessionId' => $accessToken, + 'user' => 'Admin', + 'serverId' => Uuid::uuid(), + ]); + $this->expectSuccessResponse($I); + } + + public function joinByNewSessionFormat(AuthserverSteps $I) { + $I->wantTo('join to server by legacy protocol with new launcher session format, using legacy authserver'); + list($accessToken) = $I->amAuthenticated(); + $this->route->joinLegacy([ + 'sessionId' => 'token:' . $accessToken . ':' . 'df936908-b2e1-544d-96f8-2977ec213022', + 'user' => 'Admin', + 'serverId' => Uuid::uuid(), + ]); + $this->expectSuccessResponse($I); + } + + public function joinByOauth2Token(OauthSteps $I) { + $I->wantTo('join to server using modern oAuth2 generated token with new launcher session format'); + $accessToken = $I->getAccessToken([S::MINECRAFT_SERVER_SESSION]); + $this->route->joinLegacy([ + 'sessionId' => 'token:' . $accessToken . ':' . 'df936908-b2e1-544d-96f8-2977ec213022', + 'user' => 'Admin', + 'serverId' => Uuid::uuid(), + ]); + $this->expectSuccessResponse($I); + } + + public function wrongArguments(FunctionalTester $I) { + $I->wantTo('get error on wrong amount of arguments'); + $this->route->joinLegacy([ + 'wrong' => 'argument', + ]); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseContains('credentials can not be null.'); + } + + public function joinWithWrongAccessToken(FunctionalTester $I) { + $I->wantTo('join to some server with wrong accessToken'); + $this->route->joinLegacy([ + 'sessionId' => 'token:' . Uuid::uuid() . ':' . Uuid::uuid(), + 'user' => 'random-username', + 'serverId' => Uuid::uuid(), + ]); + $I->seeResponseCodeIs(401); + $I->canSeeResponseContains('Ely.by authorization required'); + } + + public function joinWithAccessTokenWithoutMinecraftPermission(OauthSteps $I) { + $I->wantTo('join to some server with wrong accessToken'); + $accessToken = $I->getAccessToken([S::ACCOUNT_INFO]); + $this->route->joinLegacy([ + 'sessionId' => 'token:' . $accessToken . ':' . 'df936908-b2e1-544d-96f8-2977ec213022', + 'user' => 'Admin', + 'serverId' => Uuid::uuid(), + ]); + $I->seeResponseCodeIs(401); + $I->canSeeResponseContains('Ely.by authorization required'); + } + + private function expectSuccessResponse(FunctionalTester $I) { + $I->seeResponseCodeIs(200); + $I->canSeeResponseEquals('OK'); + } + +} diff --git a/tests/codeception/common/unit/helpers/StringHelperTest.php b/tests/codeception/common/unit/helpers/StringHelperTest.php index 50c7297..b1f1a21 100644 --- a/tests/codeception/common/unit/helpers/StringHelperTest.php +++ b/tests/codeception/common/unit/helpers/StringHelperTest.php @@ -15,8 +15,8 @@ class StringHelperTest extends \PHPUnit_Framework_TestCase { public function testIsUuid() { $this->assertTrue(StringHelper::isUuid('a80b4487-a5c6-45a5-9829-373b4a494135')); + $this->assertTrue(StringHelper::isUuid('a80b4487a5c645a59829373b4a494135')); $this->assertFalse(StringHelper::isUuid('12345678')); - $this->assertFalse(StringHelper::isUuid('12345678-1234-1234-1234-123456789123')); } }