Добавлена логика HasJoined для сервера авторизации Minecraft

Исправлена ошибка в JoinForm
Добавлено базовое API для общения с сервером системы скинов
This commit is contained in:
ErickSkrauch 2016-09-06 12:56:39 +03:00
parent 198e440b8d
commit 68ce8b3fb6
17 changed files with 444 additions and 4 deletions

View File

@ -4,9 +4,12 @@ namespace api\modules\session\controllers;
use api\controllers\ApiController; use api\controllers\ApiController;
use api\modules\session\exceptions\ForbiddenOperationException; use api\modules\session\exceptions\ForbiddenOperationException;
use api\modules\session\exceptions\SessionServerException; use api\modules\session\exceptions\SessionServerException;
use api\modules\session\models\HasJoinedForm;
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\LegacyJoin;
use api\modules\session\models\protocols\ModernHasJoined;
use api\modules\session\models\protocols\ModernJoin; use api\modules\session\models\protocols\ModernJoin;
use common\models\Textures;
use Yii; use Yii;
use yii\web\Response; use yii\web\Response;
@ -57,4 +60,38 @@ class SessionController extends ApiController {
return 'OK'; 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';
}
} }

View File

@ -0,0 +1,52 @@
<?php
namespace api\modules\session\models;
use api\modules\session\exceptions\ForbiddenOperationException;
use api\modules\session\exceptions\IllegalArgumentException;
use api\modules\session\models\protocols\HasJoinedInterface;
use api\modules\session\Module as Session;
use common\models\Account;
use yii\base\ErrorException;
use yii\base\Model;
class HasJoinedForm extends Model {
private $protocol;
public function __construct(HasJoinedInterface $protocol, array $config = []) {
$this->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;
}
}

View File

@ -18,9 +18,9 @@ use yii\web\UnauthorizedHttpException;
class JoinForm extends Model { class JoinForm extends Model {
private $accessToken; public $accessToken;
private $selectedProfile; public $selectedProfile;
private $serverId; public $serverId;
/** /**
* @var Account|null * @var Account|null

View File

@ -1,6 +1,7 @@
<?php <?php
namespace api\modules\session\models; namespace api\modules\session\models;
use common\models\Account;
use Yii; use Yii;
class SessionModel { class SessionModel {
@ -50,7 +51,15 @@ class SessionModel {
return Yii::$app->redis->executeCommand('DEL', [static::buildKey($this->username, $this->serverId)]); return Yii::$app->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); return md5('minecraft:join-server:' . mb_strtolower($username) . ':' . $serverId);
} }

View File

@ -0,0 +1,31 @@
<?php
namespace api\modules\session\models\protocols;
use yii\validators\RequiredValidator;
abstract class BaseHasJoined implements HasJoinedInterface {
private $username;
private $serverId;
public function __construct(string $username, string $serverId) {
$this->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);
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace api\modules\session\models\protocols;
interface HasJoinedInterface {
public function getUsername() : string;
public function getServerId() : string;
public function validate() : bool;
}

View File

@ -0,0 +1,6 @@
<?php
namespace api\modules\session\models\protocols;
class LegacyHasJoined extends BaseHasJoined {
}

View File

@ -0,0 +1,6 @@
<?php
namespace api\modules\session\models\protocols;
class ModernHasJoined extends BaseHasJoined {
}

View File

@ -19,6 +19,7 @@ class Yii extends \yii\BaseYii {
* @property \yii\swiftmailer\Mailer $mailer * @property \yii\swiftmailer\Mailer $mailer
* @property \yii\redis\Connection $redis * @property \yii\redis\Connection $redis
* @property \common\components\RabbitMQ\Component $amqp * @property \common\components\RabbitMQ\Component $amqp
* @property \GuzzleHttp\Client $guzzle
*/ */
abstract class BaseApplication extends yii\base\Application { abstract class BaseApplication extends yii\base\Application {
} }

View File

@ -0,0 +1,29 @@
<?php
namespace common\components\SkinSystem;
use GuzzleHttp\Client as GuzzleClient;
use Yii;
class Api {
const BASE_DOMAIN = 'http://skinsystem.ely.by';
public function textures($username) : array {
$response = $this->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;
}
}

View File

@ -23,6 +23,9 @@ return [
'amqp' => [ 'amqp' => [
'class' => \common\components\RabbitMQ\Component::class, 'class' => \common\components\RabbitMQ\Component::class,
], ],
'guzzle' => [
'class' => \GuzzleHttp\Client::class,
],
], ],
'aliases' => [ 'aliases' => [
'@bower' => '@vendor/bower-asset', '@bower' => '@vendor/bower-asset',

View File

@ -0,0 +1,71 @@
<?php
namespace common\models;
use common\components\SkinSystem\Api as SkinSystemApi;
class Textures {
public $displayElyMark = true;
/**
* @var Account
*/
protected $account;
public function __construct(Account $account) {
$this->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);
}
}

View File

@ -18,4 +18,14 @@ class SessionServerRoute extends BasePage {
$this->actor->sendGET($this->getUrl(), $params); $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);
}
} }

View File

@ -6,6 +6,7 @@ modules:
- tests\codeception\common\_support\FixtureHelper - tests\codeception\common\_support\FixtureHelper
- Redis - Redis
- AMQP - AMQP
- Asserts
- REST: - REST:
depends: Yii2 depends: Yii2
config: config:

View File

@ -0,0 +1,38 @@
<?php
namespace tests\codeception\api\functional\_steps;
use common\models\OauthScope as S;
use Faker\Provider\Uuid;
use tests\codeception\api\_pages\SessionServerRoute;
class SessionServerSteps extends \tests\codeception\api\FunctionalTester {
public function amJoined($byLegacy = false) {
$oauthSteps = new OauthSteps($this->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];
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace tests\codeception\api\functional\sessionserver;
use Faker\Provider\Uuid;
use tests\codeception\api\_pages\SessionServerRoute;
use tests\codeception\api\functional\_steps\SessionServerSteps;
use tests\codeception\api\FunctionalTester;
class HasJoinedCest {
/**
* @var SessionServerRoute
*/
private $route;
public function _before(FunctionalTester $I) {
$this->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.',
]);
}
}

View File

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