mirror of
https://github.com/elyby/accounts.git
synced 2024-11-30 02:32:26 +05:30
Немного рефакторинга Join формы для учёта Legacy API
Добавлена поддержка чтения данных из POST запроса, если они переданы как RAW json Исправлен StringHelper::isUuid()
This commit is contained in:
parent
34d725abe2
commit
e8b5e90a91
@ -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';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
14
api/modules/session/models/protocols/BaseJoin.php
Normal file
14
api/modules/session/models/protocols/BaseJoin.php
Normal 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;
|
||||
|
||||
}
|
15
api/modules/session/models/protocols/JoinInterface.php
Normal file
15
api/modules/session/models/protocols/JoinInterface.php
Normal 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;
|
||||
|
||||
}
|
63
api/modules/session/models/protocols/LegacyJoin.php
Normal file
63
api/modules/session/models/protocols/LegacyJoin.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
38
api/modules/session/models/protocols/ModernJoin.php
Normal file
38
api/modules/session/models/protocols/ModernJoin.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace common\helpers;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class StringHelper {
|
||||
|
||||
public static function getEmailMask(string $email) : string {
|
||||
@ -23,14 +25,18 @@ class StringHelper {
|
||||
|
||||
/**
|
||||
* Проверяет на то, что переданная строка является валидным UUID
|
||||
* Regex найдено на просторах интернета: http://stackoverflow.com/a/6223221
|
||||
*
|
||||
* @param string $uuid
|
||||
* @return 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}/';
|
||||
return preg_match($re, $uuid, $matches) === 1;
|
||||
try {
|
||||
Uuid::fromString($uuid);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,4 +13,9 @@ class SessionServerRoute extends BasePage {
|
||||
$this->actor->sendPOST($this->getUrl(), $params);
|
||||
}
|
||||
|
||||
public function joinLegacy(array $params) {
|
||||
$this->route = ['sessionserver/session/join-legacy'];
|
||||
$this->actor->sendGET($this->getUrl(), $params);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
@ -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'));
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user