Немного рефакторинга Join формы для учёта Legacy API

Добавлена поддержка чтения данных из POST запроса, если они переданы как RAW json
Исправлен StringHelper::isUuid()
This commit is contained in:
ErickSkrauch 2016-09-05 17:55:38 +03:00
parent 34d725abe2
commit e8b5e90a91
12 changed files with 338 additions and 49 deletions

View File

@ -2,7 +2,13 @@
namespace api\modules\session\controllers; namespace api\modules\session\controllers;
use api\controllers\ApiController; 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\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 { class SessionController extends ApiController {
@ -14,15 +20,41 @@ class SessionController extends ApiController {
} }
public function actionJoin() { public function actionJoin() {
$joinForm = new JoinForm(); Yii::$app->response->format = Response::FORMAT_JSON;
$joinForm->loadByPost();
$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(); $joinForm->join();
return ['id' => 'OK']; return ['id' => 'OK'];
} }
public function actionJoinLegacy() { 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';
} }
} }

View File

@ -1,27 +0,0 @@
<?php
namespace api\modules\session\models;
use Yii;
use yii\base\Model;
abstract class Form extends Model {
public function formName() {
return '';
}
public function loadByGet() {
return $this->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);
}
}

View File

@ -3,60 +3,92 @@ namespace api\modules\session\models;
use api\modules\session\exceptions\ForbiddenOperationException; use api\modules\session\exceptions\ForbiddenOperationException;
use api\modules\session\exceptions\IllegalArgumentException; use api\modules\session\exceptions\IllegalArgumentException;
use api\modules\session\models\protocols\JoinInterface;
use api\modules\session\Module as Session; use api\modules\session\Module as Session;
use api\modules\session\validators\RequiredValidator; use api\modules\session\validators\RequiredValidator;
use common\helpers\StringHelper;
use common\models\OauthScope as S; use common\models\OauthScope as S;
use common\validators\UuidValidator; use common\validators\UuidValidator;
use common\models\Account; use common\models\Account;
use common\models\MinecraftAccessKey; use common\models\MinecraftAccessKey;
use Yii; use Yii;
use yii\base\ErrorException; use yii\base\ErrorException;
use yii\base\Model;
use yii\web\UnauthorizedHttpException; use yii\web\UnauthorizedHttpException;
class JoinForm extends Form { class JoinForm extends Model {
public $accessToken; private $accessToken;
public $selectedProfile; private $selectedProfile;
public $serverId; private $serverId;
/**
* @var Account|null
*/
private $account; 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() { public function rules() {
return [ return [
[['accessToken', 'selectedProfile', 'serverId'], RequiredValidator::class], [['accessToken', 'serverId'], RequiredValidator::class],
[['accessToken', 'selectedProfile'], 'validateUuid'], [['accessToken', 'selectedProfile'], 'validateUuid'],
[['accessToken'], 'validateAccessToken'], [['accessToken'], 'validateAccessToken'],
]; ];
} }
public function join() { public function join() {
Session::info( $serverId = $this->serverId;
"User with access_token = '{$this->accessToken}' trying join to server with server_id = " . $accessToken = $this->accessToken;
"'{$this->serverId}'." Session::info("User with access_token = '{$accessToken}' trying join to server with server_id = '{$serverId}'.");
);
if (!$this->validate()) { if (!$this->validate()) {
return false; return false;
} }
$account = $this->getAccount(); $account = $this->getAccount();
$sessionModel = new SessionModel($account->username, $this->serverId); $sessionModel = new SessionModel($account->username, $serverId);
if (!$sessionModel->save()) { if (!$sessionModel->save()) {
throw new ErrorException('Cannot save join session model'); throw new ErrorException('Cannot save join session model');
} }
Session::info( Session::info(
"User with access_token = '{$this->accessToken}' and nickname = '{$account->username}' successfully " . "User with access_token = '{$accessToken}' and nickname = '{$account->username}' successfully joined to " .
"joined to server_id = '{$this->serverId}'." "server_id = '{$serverId}'."
); );
return true; 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) { public function validateUuid($attribute) {
if ($this->hasErrors($attribute)) { if ($this->hasErrors($attribute)) {
return; return;
} }
if ($attribute === 'selectedProfile' && !StringHelper::isUuid($this->selectedProfile)) {
// Это нормально. Там может быть ник игрока, если это Legacy авторизация
return;
}
$validator = new UuidValidator(); $validator = new UuidValidator();
$validator->validateAttribute($this, $attribute); $validator->validateAttribute($this, $attribute);
@ -101,12 +133,20 @@ class JoinForm extends Form {
throw new ForbiddenOperationException('Expired access_token.'); 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( 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}'." " but access_token issued to account with id = '{$account->uuid}'."
); );
throw new ForbiddenOperationException('Wrong selected_profile.'); 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; $this->account = $account;

View File

@ -0,0 +1,14 @@
<?php
namespace api\modules\session\models\protocols;
abstract class BaseJoin implements JoinInterface {
abstract public function getAccessToken() : string;
abstract public function getSelectedProfile() : string;
abstract public function getServerId() : string;
abstract public function validate() : bool;
}

View File

@ -0,0 +1,15 @@
<?php
namespace api\modules\session\models\protocols;
interface JoinInterface {
public function getAccessToken() : string;
// TODO: после перехода на PHP 7.1 сменить тип на ?string и возвращать null, если параметр не передан
public function getSelectedProfile() : string;
public function getServerId() : string;
public function validate() : bool;
}

View File

@ -0,0 +1,63 @@
<?php
namespace api\modules\session\models\protocols;
use yii\validators\RequiredValidator;
class LegacyJoin extends BaseJoin {
private $user;
private $sessionId;
private $serverId;
private $accessToken;
private $uuid;
public function __construct(string $user, string $sessionId, string $serverId) {
$this->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;
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace api\modules\session\models\protocols;
use yii\validators\RequiredValidator;
class ModernJoin extends BaseJoin {
private $accessToken;
private $selectedProfile;
private $serverId;
public function __construct(string $accessToken, string $selectedProfile, string $serverId) {
$this->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);
}
}

View File

@ -1,6 +1,8 @@
<?php <?php
namespace common\helpers; namespace common\helpers;
use Ramsey\Uuid\Uuid;
class StringHelper { class StringHelper {
public static function getEmailMask(string $email) : string { public static function getEmailMask(string $email) : string {
@ -23,14 +25,18 @@ class StringHelper {
/** /**
* Проверяет на то, что переданная строка является валидным UUID * Проверяет на то, что переданная строка является валидным UUID
* Regex найдено на просторах интернета: http://stackoverflow.com/a/6223221
* *
* @param string $uuid * @param string $uuid
* @return bool * @return bool
*/ */
public static function isUuid(string $uuid) : bool { public static function isUuid(string $uuid) : bool {
$re = '/[a-f0-9]{8}\-[a-f0-9]{4}\-4[a-f0-9]{3}\-(8|9|a|b)[a-f0-9]{3}\-[a-f0-9]{12}/'; try {
return preg_match($re, $uuid, $matches) === 1; Uuid::fromString($uuid);
} catch (\InvalidArgumentException $e) {
return false;
}
return true;
} }
} }

View File

@ -13,4 +13,9 @@ class SessionServerRoute extends BasePage {
$this->actor->sendPOST($this->getUrl(), $params); $this->actor->sendPOST($this->getUrl(), $params);
} }
public function joinLegacy(array $params) {
$this->route = ['sessionserver/session/join-legacy'];
$this->actor->sendGET($this->getUrl(), $params);
}
} }

View File

@ -30,8 +30,19 @@ class JoinCest {
$this->expectSuccessResponse($I); $this->expectSuccessResponse($I);
} }
public function joinByModernOauth2Token(OauthSteps $I) { public function joinByPassJsonInPost(AuthserverSteps $I) {
$I->wantTo('join to server, using moder oAuth2 generated token'); $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]); $accessToken = $I->getAccessToken([S::MINECRAFT_SERVER_SESSION]);
$this->route->join([ $this->route->join([
'accessToken' => $accessToken, 'accessToken' => $accessToken,

View File

@ -0,0 +1,92 @@
<?php
namespace tests\codeception\api\functional\sessionserver;
use common\models\OauthScope as S;
use Faker\Provider\Uuid;
use tests\codeception\api\_pages\SessionServerRoute;
use tests\codeception\api\functional\_steps\AuthserverSteps;
use tests\codeception\api\functional\_steps\OauthSteps;
use tests\codeception\api\FunctionalTester;
class JoinLegacyCest {
/**
* @var SessionServerRoute
*/
private $route;
public function _before(AuthserverSteps $I) {
$this->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');
}
}

View File

@ -15,8 +15,8 @@ class StringHelperTest extends \PHPUnit_Framework_TestCase {
public function testIsUuid() { public function testIsUuid() {
$this->assertTrue(StringHelper::isUuid('a80b4487-a5c6-45a5-9829-373b4a494135')); $this->assertTrue(StringHelper::isUuid('a80b4487-a5c6-45a5-9829-373b4a494135'));
$this->assertTrue(StringHelper::isUuid('a80b4487a5c645a59829373b4a494135'));
$this->assertFalse(StringHelper::isUuid('12345678')); $this->assertFalse(StringHelper::isUuid('12345678'));
$this->assertFalse(StringHelper::isUuid('12345678-1234-1234-1234-123456789123'));
} }
} }