mirror of
https://github.com/elyby/accounts.git
synced 2024-12-26 23:20:18 +05:30
Merge branch 'session_server'
This commit is contained in:
commit
f712c07694
@ -7,6 +7,7 @@ use yii\web\User as YiiUserComponent;
|
||||
* @property Identity|null $identity
|
||||
*
|
||||
* @method Identity|null getIdentity()
|
||||
* @method Identity|null loginByAccessToken(string $token, $type = null)
|
||||
*/
|
||||
class Component extends YiiUserComponent {
|
||||
|
||||
|
@ -2,11 +2,13 @@
|
||||
namespace api\components;
|
||||
|
||||
use api\modules\authserver\exceptions\AuthserverException;
|
||||
use api\modules\session\exceptions\SessionServerException;
|
||||
use Yii;
|
||||
|
||||
class ErrorHandler extends \yii\web\ErrorHandler {
|
||||
|
||||
public function convertExceptionToArray($exception) {
|
||||
if ($exception instanceof AuthserverException) {
|
||||
if ($exception instanceof AuthserverException || $exception instanceof SessionServerException) {
|
||||
return [
|
||||
'error' => $exception->getName(),
|
||||
'errorMessage' => $exception->getMessage(),
|
||||
@ -16,4 +18,14 @@ class ErrorHandler extends \yii\web\ErrorHandler {
|
||||
return parent::convertExceptionToArray($exception);
|
||||
}
|
||||
|
||||
public function logException($exception) {
|
||||
if ($exception instanceof AuthserverException) {
|
||||
Yii::error($exception, AuthserverException::class . ':' . $exception->getName());
|
||||
} elseif ($exception instanceof SessionServerException) {
|
||||
Yii::error($exception, SessionServerException::class . ':' . $exception->getName());
|
||||
} else {
|
||||
parent::logException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ use yii\web\User as YiiUserComponent;
|
||||
* @property AccountSession|null $activeSession
|
||||
* @property AccountIdentity|null $identity
|
||||
*
|
||||
* @method AccountIdentity|null getIdentity()
|
||||
* @method AccountIdentity|null getIdentity($autoRenew = true)
|
||||
*/
|
||||
class Component extends YiiUserComponent {
|
||||
|
||||
|
@ -26,6 +26,24 @@ return [
|
||||
[
|
||||
'class' => \yii\log\FileTarget::class,
|
||||
'levels' => ['error', 'warning'],
|
||||
'except' => [
|
||||
'legacy-authserver',
|
||||
'session',
|
||||
'api\modules\session\exceptions\SessionServerException:*',
|
||||
'api\modules\authserver\exceptions\AuthserverException:*',
|
||||
],
|
||||
],
|
||||
[
|
||||
'class' => \yii\log\FileTarget::class,
|
||||
'levels' => ['error', 'info'],
|
||||
'categories' => ['legacy-authserver'],
|
||||
'logFile' => '@runtime/logs/authserver.log',
|
||||
],
|
||||
[
|
||||
'class' => \yii\log\FileTarget::class,
|
||||
'levels' => ['error', 'info'],
|
||||
'categories' => ['session'],
|
||||
'logFile' => '@runtime/logs/session.log',
|
||||
],
|
||||
],
|
||||
],
|
||||
@ -56,5 +74,8 @@ return [
|
||||
'class' => \api\modules\authserver\Module::class,
|
||||
'baseDomain' => $params['authserverDomain'],
|
||||
],
|
||||
'session' => [
|
||||
'class' => \api\modules\session\Module::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
@ -7,4 +7,10 @@ return [
|
||||
'/oauth2/v1/<action>' => 'oauth/<action>',
|
||||
|
||||
'/account/v1/info' => 'identity-info/index',
|
||||
|
||||
'/minecraft/session/join' => 'session/session/join',
|
||||
'/minecraft/session/legacy/join' => 'session/session/join-legacy',
|
||||
'/minecraft/session/hasJoined' => 'session/session/has-joined',
|
||||
'/minecraft/session/legacy/hasJoined' => 'session/session/has-joined-legacy',
|
||||
'/minecraft/session/profile/<uuid>' => 'session/session/profile',
|
||||
];
|
||||
|
@ -4,6 +4,7 @@ namespace api\modules\authserver;
|
||||
use Yii;
|
||||
use yii\base\BootstrapInterface;
|
||||
use yii\base\InvalidConfigException;
|
||||
use yii\web\NotFoundHttpException;
|
||||
|
||||
class Module extends \yii\base\Module implements BootstrapInterface {
|
||||
|
||||
@ -23,6 +24,16 @@ class Module extends \yii\base\Module implements BootstrapInterface {
|
||||
}
|
||||
}
|
||||
|
||||
public function beforeAction($action) {
|
||||
if (!parent::beforeAction($action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->checkHost();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \yii\base\Application $app the application currently running
|
||||
*/
|
||||
@ -40,4 +51,17 @@ class Module extends \yii\base\Module implements BootstrapInterface {
|
||||
Yii::info($message, 'legacy-authserver');
|
||||
}
|
||||
|
||||
/**
|
||||
* Поскольку это legacy метод и документации в новой среде для него не будет,
|
||||
* нет смысла выставлять на показ внутренние url, так что ограничиваем доступ
|
||||
* только для заходов по старому домену
|
||||
*
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
protected function checkHost() {
|
||||
if (Yii::$app->request->getHostInfo() !== $this->baseDomain) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ class ValidateForm extends Form {
|
||||
throw new ForbiddenOperationException('Invalid token.');
|
||||
}
|
||||
|
||||
if (!$result->isActual()) {
|
||||
if ($result->isExpired()) {
|
||||
$result->delete();
|
||||
throw new ForbiddenOperationException('Token expired.');
|
||||
}
|
||||
|
20
api/modules/session/Module.php
Normal file
20
api/modules/session/Module.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace api\modules\session;
|
||||
|
||||
use Yii;
|
||||
|
||||
class Module extends \yii\base\Module {
|
||||
|
||||
public $id = 'session';
|
||||
|
||||
public $defaultRoute = 'session';
|
||||
|
||||
public static function info($message) {
|
||||
Yii::info($message, 'session');
|
||||
}
|
||||
|
||||
public static function error($message) {
|
||||
Yii::info($message, 'session');
|
||||
}
|
||||
|
||||
}
|
120
api/modules/session/controllers/SessionController.php
Normal file
120
api/modules/session/controllers/SessionController.php
Normal file
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
namespace api\modules\session\controllers;
|
||||
|
||||
use api\controllers\ApiController;
|
||||
use api\modules\session\exceptions\ForbiddenOperationException;
|
||||
use api\modules\session\exceptions\IllegalArgumentException;
|
||||
use api\modules\session\exceptions\SessionServerException;
|
||||
use api\modules\session\filters\RateLimiter;
|
||||
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\Account;
|
||||
use common\models\Textures;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Yii;
|
||||
use yii\web\Response;
|
||||
|
||||
class SessionController extends ApiController {
|
||||
|
||||
public function behaviors() {
|
||||
$behaviors = parent::behaviors();
|
||||
unset($behaviors['authenticator']);
|
||||
$behaviors['rateLimiting'] = [
|
||||
'class' => RateLimiter::class,
|
||||
'only' => ['has-joined', 'has-joined-legacy'],
|
||||
];
|
||||
|
||||
return $behaviors;
|
||||
}
|
||||
|
||||
public function actionJoin() {
|
||||
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';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
public function actionProfile($uuid) {
|
||||
try {
|
||||
$uuid = Uuid::fromString($uuid)->toString();
|
||||
} catch(\InvalidArgumentException $e) {
|
||||
throw new IllegalArgumentException('Invalid uuid format.');
|
||||
}
|
||||
|
||||
$account = Account::findOne(['uuid' => $uuid]);
|
||||
if ($account === null) {
|
||||
throw new ForbiddenOperationException('Invalid uuid.');
|
||||
}
|
||||
|
||||
return (new Textures($account))->getMinecraftResponse();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace api\modules\session\exceptions;
|
||||
|
||||
class ForbiddenOperationException extends SessionServerException {
|
||||
|
||||
public function __construct($message, $code = 0, \Exception $previous = null) {
|
||||
parent::__construct($status = 401, $message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
10
api/modules/session/exceptions/IllegalArgumentException.php
Normal file
10
api/modules/session/exceptions/IllegalArgumentException.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace api\modules\session\exceptions;
|
||||
|
||||
class IllegalArgumentException extends SessionServerException {
|
||||
|
||||
public function __construct($message = 'credentials can not be null.', $code = 0, \Exception $previous = null) {
|
||||
parent::__construct(400, $message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
19
api/modules/session/exceptions/SessionServerException.php
Normal file
19
api/modules/session/exceptions/SessionServerException.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace api\modules\session\exceptions;
|
||||
|
||||
use ReflectionClass;
|
||||
use yii\web\HttpException;
|
||||
|
||||
class SessionServerException extends HttpException {
|
||||
|
||||
/**
|
||||
* Рефлексия быстрее, как ни странно:
|
||||
* @url https://coderwall.com/p/cpxxxw/php-get-class-name-without-namespace#comment_19313
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
return (new ReflectionClass($this))->getShortName();
|
||||
}
|
||||
|
||||
}
|
109
api/modules/session/filters/RateLimiter.php
Normal file
109
api/modules/session/filters/RateLimiter.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
namespace api\modules\session\filters;
|
||||
|
||||
use common\models\OauthClient;
|
||||
use Yii;
|
||||
use yii\base\InvalidConfigException;
|
||||
use yii\web\Request;
|
||||
use yii\web\TooManyRequestsHttpException;
|
||||
|
||||
class RateLimiter extends \yii\filters\RateLimiter {
|
||||
|
||||
public $limit = 180;
|
||||
public $limitTime = 3600; // 1h
|
||||
|
||||
public $authserverDomain;
|
||||
|
||||
private $server;
|
||||
|
||||
public function init() {
|
||||
parent::init();
|
||||
if ($this->authserverDomain === null) {
|
||||
$this->authserverDomain = Yii::$app->params['authserverDomain'] ?? null;
|
||||
}
|
||||
|
||||
if ($this->authserverDomain === null) {
|
||||
throw new InvalidConfigException('authserverDomain param is required');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function beforeAction($action) {
|
||||
$this->checkRateLimit(
|
||||
null,
|
||||
$this->request ?: Yii::$app->getRequest(),
|
||||
$this->response ?: Yii::$app->getResponse(),
|
||||
$action
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function checkRateLimit($user, $request, $response, $action) {
|
||||
if ($request->getHostInfo() === $this->authserverDomain) {
|
||||
return;
|
||||
}
|
||||
|
||||
$server = $this->getServer($request);
|
||||
if ($server !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ip = $request->getUserIP();
|
||||
$key = $this->buildKey($ip);
|
||||
|
||||
$redis = $this->getRedis();
|
||||
$countRequests = intval($redis->executeCommand('INCR', [$key]));
|
||||
if ($countRequests === 1) {
|
||||
$redis->executeCommand('EXPIRE', [$key, $this->limitTime]);
|
||||
}
|
||||
|
||||
if ($countRequests > $this->limit) {
|
||||
throw new TooManyRequestsHttpException($this->errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \yii\redis\Connection
|
||||
*/
|
||||
public function getRedis() {
|
||||
return Yii::$app->redis;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return OauthClient|null
|
||||
*/
|
||||
protected function getServer(Request $request) {
|
||||
$serverId = $request->get('server_id');
|
||||
if ($serverId === null) {
|
||||
$this->server = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->server === null) {
|
||||
/** @var OauthClient $server */
|
||||
$this->server = OauthClient::findOne($serverId);
|
||||
// TODO: убедится, что это сервер
|
||||
if ($this->server === null) {
|
||||
$this->server = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->server === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->server;
|
||||
}
|
||||
|
||||
protected function buildKey($ip) : string {
|
||||
return 'sessionserver:ratelimit:' . $ip;
|
||||
}
|
||||
|
||||
}
|
52
api/modules/session/models/HasJoinedForm.php
Normal file
52
api/modules/session/models/HasJoinedForm.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
162
api/modules/session/models/JoinForm.php
Normal file
162
api/modules/session/models/JoinForm.php
Normal file
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
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 Model {
|
||||
|
||||
public $accessToken;
|
||||
public $selectedProfile;
|
||||
public $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', 'serverId'], RequiredValidator::class],
|
||||
[['accessToken', 'selectedProfile'], 'validateUuid'],
|
||||
[['accessToken'], 'validateAccessToken'],
|
||||
];
|
||||
}
|
||||
|
||||
public function join() {
|
||||
$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, $serverId);
|
||||
if (!$sessionModel->save()) {
|
||||
throw new ErrorException('Cannot save join session model');
|
||||
}
|
||||
|
||||
Session::info(
|
||||
"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);
|
||||
|
||||
if ($this->hasErrors($attribute)) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \api\modules\session\exceptions\SessionServerException
|
||||
*/
|
||||
public function validateAccessToken() {
|
||||
$accessToken = $this->accessToken;
|
||||
/** @var MinecraftAccessKey|null $accessModel */
|
||||
$accessModel = MinecraftAccessKey::findOne($accessToken);
|
||||
if ($accessModel === null) {
|
||||
try {
|
||||
$identity = Yii::$app->apiUser->loginByAccessToken($accessToken);
|
||||
} catch (UnauthorizedHttpException $e) {
|
||||
$identity = null;
|
||||
}
|
||||
|
||||
if ($identity === null) {
|
||||
Session::error("User with access_token = '{$accessToken}' failed join by wrong access_token.");
|
||||
throw new ForbiddenOperationException('Invalid access_token.');
|
||||
}
|
||||
|
||||
if (!Yii::$app->apiUser->can(S::MINECRAFT_SERVER_SESSION)) {
|
||||
Session::error("User with access_token = '{$accessToken}' doesn't have enough scopes to make join.");
|
||||
throw new ForbiddenOperationException('The token does not have required scope.');
|
||||
}
|
||||
|
||||
$accessModel = $identity->getAccessToken();
|
||||
$account = $identity->getAccount();
|
||||
} else {
|
||||
$account = $accessModel->account;
|
||||
}
|
||||
|
||||
/** @var MinecraftAccessKey|\common\models\OauthAccessToken $accessModel */
|
||||
if ($accessModel->isExpired()) {
|
||||
Session::error("User with access_token = '{$accessToken}' failed join by expired access_token.");
|
||||
throw new ForbiddenOperationException('Expired access_token.');
|
||||
}
|
||||
|
||||
$selectedProfile = $this->selectedProfile;
|
||||
$isUuid = StringHelper::isUuid($selectedProfile);
|
||||
if ($isUuid && $account->uuid !== $selectedProfile) {
|
||||
Session::error(
|
||||
"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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Account|null
|
||||
*/
|
||||
protected function getAccount() {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
}
|
66
api/modules/session/models/SessionModel.php
Normal file
66
api/modules/session/models/SessionModel.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
namespace api\modules\session\models;
|
||||
|
||||
use common\models\Account;
|
||||
use Yii;
|
||||
|
||||
class SessionModel {
|
||||
|
||||
const KEY_TIME = 120; // 2 min
|
||||
|
||||
public $username;
|
||||
|
||||
public $serverId;
|
||||
|
||||
public function __construct(string $username, string $serverId) {
|
||||
$this->username = $username;
|
||||
$this->serverId = $serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $username
|
||||
* @param $serverId
|
||||
*
|
||||
* @return static|null
|
||||
*/
|
||||
public static function find($username, $serverId) {
|
||||
$key = static::buildKey($username, $serverId);
|
||||
$result = Yii::$app->redis->executeCommand('GET', [$key]);
|
||||
if (!$result) {
|
||||
/** @noinspection PhpIncompatibleReturnTypeInspection шторм что-то сума сходит, когда видит static */
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($result, true);
|
||||
$model = new static($data['username'], $data['serverId']);
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
public function save() {
|
||||
$key = static::buildKey($this->username, $this->serverId);
|
||||
$data = json_encode([
|
||||
'username' => $this->username,
|
||||
'serverId' => $this->serverId,
|
||||
]);
|
||||
|
||||
return Yii::$app->redis->executeCommand('SETEX', [$key, self::KEY_TIME, $data]);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
return Yii::$app->redis->executeCommand('DEL', [static::buildKey($this->username, $this->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);
|
||||
}
|
||||
|
||||
}
|
31
api/modules/session/models/protocols/BaseHasJoined.php
Normal file
31
api/modules/session/models/protocols/BaseHasJoined.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
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;
|
||||
|
||||
}
|
12
api/modules/session/models/protocols/HasJoinedInterface.php
Normal file
12
api/modules/session/models/protocols/HasJoinedInterface.php
Normal 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;
|
||||
|
||||
}
|
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;
|
||||
|
||||
}
|
6
api/modules/session/models/protocols/LegacyHasJoined.php
Normal file
6
api/modules/session/models/protocols/LegacyHasJoined.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace api\modules\session\models\protocols;
|
||||
|
||||
class LegacyHasJoined extends BaseHasJoined {
|
||||
|
||||
}
|
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
6
api/modules/session/models/protocols/ModernHasJoined.php
Normal file
6
api/modules/session/models/protocols/ModernHasJoined.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace api\modules\session\models\protocols;
|
||||
|
||||
class ModernHasJoined extends BaseHasJoined {
|
||||
|
||||
}
|
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);
|
||||
}
|
||||
|
||||
}
|
25
api/modules/session/validators/RequiredValidator.php
Normal file
25
api/modules/session/validators/RequiredValidator.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
namespace api\modules\session\validators;
|
||||
|
||||
use api\modules\session\exceptions\IllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Для данного модуля нам не принципиально, что там за ошибка: если не хватает хотя бы одного
|
||||
* параметра - тут же отправляем исключение и дело с концом
|
||||
*/
|
||||
class RequiredValidator extends \yii\validators\RequiredValidator {
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return null
|
||||
* @throws \api\modules\session\exceptions\SessionServerException
|
||||
*/
|
||||
protected function validateValue($value) {
|
||||
if (parent::validateValue($value) !== null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -19,6 +19,7 @@ class Yii extends \yii\BaseYii {
|
||||
* @property \yii\swiftmailer\Mailer $mailer
|
||||
* @property \yii\redis\Connection $redis
|
||||
* @property \common\components\RabbitMQ\Component $amqp
|
||||
* @property \GuzzleHttp\Client $guzzle
|
||||
*/
|
||||
abstract class BaseApplication extends yii\base\Application {
|
||||
}
|
||||
|
29
common/components/SkinSystem/Api.php
Normal file
29
common/components/SkinSystem/Api.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -3,7 +3,8 @@ return [
|
||||
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
|
||||
'components' => [
|
||||
'cache' => [
|
||||
'class' => yii\caching\FileCache::class,
|
||||
'class' => yii\redis\Cache::class,
|
||||
'redis' => 'redis',
|
||||
],
|
||||
'db' => [
|
||||
'class' => yii\db\Connection::class,
|
||||
@ -22,6 +23,9 @@ return [
|
||||
'amqp' => [
|
||||
'class' => \common\components\RabbitMQ\Component::class,
|
||||
],
|
||||
'guzzle' => [
|
||||
'class' => \GuzzleHttp\Client::class,
|
||||
],
|
||||
],
|
||||
'aliases' => [
|
||||
'@bower' => '@vendor/bower-asset',
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -53,8 +53,8 @@ class MinecraftAccessKey extends ActiveRecord {
|
||||
return $this->hasOne(Account::class, ['id' => 'account_id']);
|
||||
}
|
||||
|
||||
public function isActual() : bool {
|
||||
return $this->updated_at + self::LIFETIME >= time();
|
||||
public function isExpired() : bool {
|
||||
return time() > $this->updated_at + self::LIFETIME;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class OauthAccessToken extends ActiveRecord {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isExpired() {
|
||||
public function isExpired() : bool {
|
||||
return time() > $this->expire_time;
|
||||
}
|
||||
|
||||
|
71
common/models/Textures.php
Normal file
71
common/models/Textures.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
23
common/validators/UuidValidator.php
Normal file
23
common/validators/UuidValidator.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace common\validators;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use yii\validators\Validator;
|
||||
|
||||
class UuidValidator extends Validator {
|
||||
|
||||
public $skipOnEmpty = false;
|
||||
|
||||
public $message = '{attribute} must be valid uuid';
|
||||
|
||||
public function validateAttribute($model, $attribute) {
|
||||
try {
|
||||
$uuid = Uuid::fromString($model->$attribute)->toString();
|
||||
$model->$attribute = $uuid;
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->addError($model, $attribute, $this->message, []);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
FROM nginx:1.9
|
||||
FROM nginx:1.11
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY account.ely.by.conf.template /etc/nginx/conf.d/account.ely.by.conf.template
|
||||
|
@ -4,7 +4,6 @@ server {
|
||||
set $root_path '/var/www/html';
|
||||
set $api_path '${root_path}/api/web';
|
||||
set $frontend_path '${root_path}/frontend/dist';
|
||||
set $authserver_host '${AUTHSERVER_HOST}';
|
||||
|
||||
root $root_path;
|
||||
charset utf-8;
|
||||
@ -12,15 +11,19 @@ server {
|
||||
etag on;
|
||||
|
||||
set $request_url $request_uri;
|
||||
if ($host = $authserver_host) {
|
||||
set $host_with_uri '${host}${request_uri}';
|
||||
|
||||
if ($host_with_uri ~* '^${AUTHSERVER_HOST}/auth') {
|
||||
set $request_url '/api/authserver${request_uri}';
|
||||
rewrite ^/auth /api/authserver$uri last;
|
||||
}
|
||||
|
||||
if ($host_with_uri ~* '^${AUTHSERVER_HOST}/session') {
|
||||
set $request_url '/api/minecraft${request_uri}';
|
||||
rewrite ^/session /api/minecraft$uri last;
|
||||
}
|
||||
|
||||
location / {
|
||||
if ($host = $authserver_host) {
|
||||
rewrite ^ /api/authserver$uri last;
|
||||
}
|
||||
|
||||
alias $frontend_path;
|
||||
index index.html;
|
||||
try_files $uri /index.html =404;
|
||||
|
36
tests/codeception/api/_pages/SessionServerRoute.php
Normal file
36
tests/codeception/api/_pages/SessionServerRoute.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
namespace tests\codeception\api\_pages;
|
||||
|
||||
use yii\codeception\BasePage;
|
||||
|
||||
/**
|
||||
* @property \tests\codeception\api\FunctionalTester $actor
|
||||
*/
|
||||
class SessionServerRoute extends BasePage {
|
||||
|
||||
public function join($params) {
|
||||
$this->route = '/minecraft/session/join';
|
||||
$this->actor->sendPOST($this->getUrl(), $params);
|
||||
}
|
||||
|
||||
public function joinLegacy(array $params) {
|
||||
$this->route = '/minecraft/session/legacy/join';
|
||||
$this->actor->sendGET($this->getUrl(), $params);
|
||||
}
|
||||
|
||||
public function hasJoined(array $params) {
|
||||
$this->route = '/minecraft/session/hasJoined';
|
||||
$this->actor->sendGET($this->getUrl(), $params);
|
||||
}
|
||||
|
||||
public function hasJoinedLegacy(array $params) {
|
||||
$this->route = '/minecraft/session/legacy/hasJoined';
|
||||
$this->actor->sendGET($this->getUrl(), $params);
|
||||
}
|
||||
|
||||
public function profile($profileUuid) {
|
||||
$this->route = '/minecraft/session/profile/' . $profileUuid;
|
||||
$this->actor->sendGET($this->getUrl());
|
||||
}
|
||||
|
||||
}
|
@ -6,6 +6,7 @@ modules:
|
||||
- tests\codeception\common\_support\FixtureHelper
|
||||
- Redis
|
||||
- AMQP
|
||||
- Asserts
|
||||
- REST:
|
||||
depends: Yii2
|
||||
config:
|
||||
|
@ -0,0 +1,66 @@
|
||||
<?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];
|
||||
}
|
||||
|
||||
public function canSeeValidTexturesResponse($expectedUsername, $expectedUuid) {
|
||||
$this->seeResponseIsJson();
|
||||
$this->canSeeResponseContainsJson([
|
||||
'name' => $expectedUsername,
|
||||
'id' => $expectedUuid,
|
||||
'ely' => true,
|
||||
'properties' => [
|
||||
[
|
||||
'name' => 'textures',
|
||||
'signature' => 'Cg==',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->canSeeResponseJsonMatchesJsonPath('$.properties[0].value');
|
||||
$value = json_decode($this->grabResponse(), true)['properties'][0]['value'];
|
||||
$decoded = json_decode(base64_decode($value), true);
|
||||
$this->assertArrayHasKey('timestamp', $decoded);
|
||||
$this->assertArrayHasKey('textures', $decoded);
|
||||
$this->assertEquals($expectedUuid, $decoded['profileId']);
|
||||
$this->assertEquals($expectedUsername, $decoded['profileName']);
|
||||
$this->assertTrue($decoded['ely']);
|
||||
$textures = $decoded['textures'];
|
||||
$this->assertArrayHasKey('SKIN', $textures);
|
||||
$skinTextures = $textures['SKIN'];
|
||||
$this->assertArrayHasKey('url', $skinTextures);
|
||||
$this->assertArrayHasKey('hash', $skinTextures);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
<?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->canSeeValidTexturesResponse($username, 'df936908b2e1544d96f82977ec213022');
|
||||
}
|
||||
|
||||
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.',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
122
tests/codeception/api/functional/sessionserver/JoinCest.php
Normal file
122
tests/codeception/api/functional/sessionserver/JoinCest.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?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 JoinCest {
|
||||
|
||||
/**
|
||||
* @var SessionServerRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(AuthserverSteps $I) {
|
||||
$this->route = new SessionServerRoute($I);
|
||||
}
|
||||
|
||||
public function joinByLegacyAuthserver(AuthserverSteps $I) {
|
||||
$I->wantTo('join to server, using legacy authserver access token');
|
||||
list($accessToken) = $I->amAuthenticated();
|
||||
$this->route->join([
|
||||
'accessToken' => $accessToken,
|
||||
'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022',
|
||||
'serverId' => Uuid::uuid(),
|
||||
]);
|
||||
$this->expectSuccessResponse($I);
|
||||
}
|
||||
|
||||
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,
|
||||
'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022',
|
||||
'serverId' => Uuid::uuid(),
|
||||
]);
|
||||
$this->expectSuccessResponse($I);
|
||||
}
|
||||
|
||||
public function joinByModernOauth2TokenWithoutPermission(OauthSteps $I) {
|
||||
$I->wantTo('join to server, using moder oAuth2 generated token, but without minecraft auth permission');
|
||||
$accessToken = $I->getAccessToken([S::ACCOUNT_INFO, S::ACCOUNT_EMAIL]);
|
||||
$this->route->join([
|
||||
'accessToken' => $accessToken,
|
||||
'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022',
|
||||
'serverId' => Uuid::uuid(),
|
||||
]);
|
||||
$I->seeResponseCodeIs(401);
|
||||
$I->seeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'The token does not have required scope.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function joinWithExpiredToken(FunctionalTester $I) {
|
||||
$I->wantTo('join to some server with expired accessToken');
|
||||
$this->route->join([
|
||||
'accessToken' => '6042634a-a1e2-4aed-866c-c661fe4e63e2',
|
||||
'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022',
|
||||
'serverId' => Uuid::uuid(),
|
||||
]);
|
||||
$I->seeResponseCodeIs(401);
|
||||
$I->seeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'Expired access_token.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function wrongArguments(FunctionalTester $I) {
|
||||
$I->wantTo('get error on wrong amount of arguments');
|
||||
$this->route->join([
|
||||
'wrong' => 'argument',
|
||||
]);
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'IllegalArgumentException',
|
||||
'errorMessage' => 'credentials can not be null.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function joinWithWrongAccessToken(FunctionalTester $I) {
|
||||
$I->wantTo('join to some server with wrong accessToken');
|
||||
$this->route->join([
|
||||
'accessToken' => Uuid::uuid(),
|
||||
'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022',
|
||||
'serverId' => Uuid::uuid(),
|
||||
]);
|
||||
$I->seeResponseCodeIs(401);
|
||||
$I->seeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'Invalid access_token.',
|
||||
]);
|
||||
}
|
||||
|
||||
private function expectSuccessResponse(FunctionalTester $I) {
|
||||
$I->seeResponseCodeIs(200);
|
||||
$I->seeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'id' => 'OK',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
<?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 ProfileCest {
|
||||
|
||||
/**
|
||||
* @var SessionServerRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(FunctionalTester $I) {
|
||||
$this->route = new SessionServerRoute($I);
|
||||
}
|
||||
|
||||
public function getProfile(SessionServerSteps $I) {
|
||||
$I->wantTo('get info about player textures by uuid');
|
||||
$this->route->profile('df936908-b2e1-544d-96f8-2977ec213022');
|
||||
$I->canSeeValidTexturesResponse('Admin', 'df936908b2e1544d96f82977ec213022');
|
||||
}
|
||||
|
||||
public function getProfileByUuidWithoutDashes(SessionServerSteps $I) {
|
||||
$I->wantTo('get info about player textures by uuid without dashes');
|
||||
$this->route->profile('df936908b2e1544d96f82977ec213022');
|
||||
$I->canSeeValidTexturesResponse('Admin', 'df936908b2e1544d96f82977ec213022');
|
||||
}
|
||||
|
||||
public function directCallWithoutUuidPart(FunctionalTester $I) {
|
||||
$I->wantTo('call profile route without passing uuid');
|
||||
$this->route->profile('');
|
||||
$I->canSeeResponseCodeIs(404);
|
||||
}
|
||||
|
||||
public function callWithInvalidUuid(FunctionalTester $I) {
|
||||
$I->wantTo('call profile route with invalid uuid string');
|
||||
$this->route->profile('bla-bla-bla');
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'IllegalArgumentException',
|
||||
'errorMessage' => 'Invalid uuid format.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getProfileWithNonexistentUuid(FunctionalTester $I) {
|
||||
$I->wantTo('get info about nonexistent uuid');
|
||||
$this->route->profile(Uuid::uuid());
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->seeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'error' => 'ForbiddenOperationException',
|
||||
'errorMessage' => 'Invalid uuid.',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
namespace tests\codeception\api\unit\modules\session\filters;
|
||||
|
||||
use api\modules\session\filters\RateLimiter;
|
||||
use common\models\OauthClient;
|
||||
use Faker\Provider\Internet;
|
||||
use tests\codeception\api\unit\TestCase;
|
||||
use Yii;
|
||||
use yii\redis\Connection;
|
||||
use yii\web\Request;
|
||||
|
||||
class RateLimiterTest extends TestCase {
|
||||
|
||||
public function testCheckRateLimiterWithOldAuthserver() {
|
||||
/** @var Connection|\PHPUnit_Framework_MockObject_MockObject $redis */
|
||||
$redis = $this->getMockBuilder(Connection::class)
|
||||
->setMethods(['executeCommand'])
|
||||
->getMock();
|
||||
|
||||
$redis->expects($this->never())
|
||||
->method('executeCommand');
|
||||
|
||||
Yii::$app->set('redis', $redis);
|
||||
|
||||
/** @var RateLimiter|\PHPUnit_Framework_MockObject_MockObject $filter */
|
||||
$filter = $this->getMockBuilder(RateLimiter::class)
|
||||
->setMethods(['getServer'])
|
||||
->getMock();
|
||||
|
||||
$filter->expects($this->any())
|
||||
->method('getServer')
|
||||
->will($this->returnValue(new OauthClient()));
|
||||
|
||||
$filter->checkRateLimit(null, new Request(), null, null);
|
||||
}
|
||||
|
||||
public function testCheckRateLimiterWithValidServerId() {
|
||||
/** @var Connection|\PHPUnit_Framework_MockObject_MockObject $redis */
|
||||
$redis = $this->getMockBuilder(Connection::class)
|
||||
->setMethods(['executeCommand'])
|
||||
->getMock();
|
||||
|
||||
$redis->expects($this->never())
|
||||
->method('executeCommand');
|
||||
|
||||
Yii::$app->set('redis', $redis);
|
||||
|
||||
/** @var Request|\PHPUnit_Framework_MockObject_MockObject $request */
|
||||
$request = $this->getMockBuilder(Request::class)
|
||||
->setMethods(['getHostInfo'])
|
||||
->getMock();
|
||||
|
||||
$request->expects($this->any())
|
||||
->method('getHostInfo')
|
||||
->will($this->returnValue('http://authserver.ely.by'));
|
||||
|
||||
$filter = new RateLimiter();
|
||||
$filter->checkRateLimit(null, $request, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \yii\web\TooManyRequestsHttpException
|
||||
*/
|
||||
public function testCheckRateLimiter() {
|
||||
/** @var Connection|\PHPUnit_Framework_MockObject_MockObject $redis */
|
||||
$redis = $this->getMockBuilder(Connection::class)
|
||||
->setMethods(['executeCommand'])
|
||||
->getMock();
|
||||
|
||||
$redis->expects($this->exactly(5))
|
||||
->method('executeCommand')
|
||||
->will($this->onConsecutiveCalls('1', '1', '2', '3', '4'));
|
||||
|
||||
Yii::$app->set('redis', $redis);
|
||||
|
||||
/** @var Request|\PHPUnit_Framework_MockObject_MockObject $request */
|
||||
$request = $this->getMockBuilder(Request::class)
|
||||
->setMethods(['getUserIP'])
|
||||
->getMock();
|
||||
|
||||
$request->expects($this->any())
|
||||
->method('getUserIp')
|
||||
->will($this->returnValue(Internet::localIpv4()));
|
||||
|
||||
/** @var RateLimiter|\PHPUnit_Framework_MockObject_MockObject $filter */
|
||||
$filter = $this->getMockBuilder(RateLimiter::class)
|
||||
->setConstructorArgs([[
|
||||
'limit' => 3,
|
||||
]])
|
||||
->setMethods(['getServer'])
|
||||
->getMock();
|
||||
|
||||
$filter->expects($this->any())
|
||||
->method('getServer')
|
||||
->will($this->returnValue(null));
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$filter->checkRateLimit(null, $request, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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'));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
namespace tests\codeception\common\unit\validators;
|
||||
|
||||
use Codeception\Specify;
|
||||
use common\validators\UuidValidator;
|
||||
use Faker\Provider\Uuid;
|
||||
use tests\codeception\common\unit\TestCase;
|
||||
use yii\base\Model;
|
||||
|
||||
class UuidValidatorTest extends TestCase {
|
||||
use Specify;
|
||||
|
||||
public function testValidateAttribute() {
|
||||
$this->specify('expected error if passed empty value', function() {
|
||||
$model = new UuidTestModel();
|
||||
expect($model->validate())->false();
|
||||
expect($model->getErrors('attribute'))->equals(['Attribute must be valid uuid']);
|
||||
});
|
||||
|
||||
$this->specify('expected error if passed invalid string', function() {
|
||||
$model = new UuidTestModel();
|
||||
$model->attribute = '123456789';
|
||||
expect($model->validate())->false();
|
||||
expect($model->getErrors('attribute'))->equals(['Attribute must be valid uuid']);
|
||||
});
|
||||
|
||||
$this->specify('no errors if passed valid uuid', function() {
|
||||
$model = new UuidTestModel();
|
||||
$model->attribute = Uuid::uuid();
|
||||
expect($model->validate())->true();
|
||||
});
|
||||
|
||||
$this->specify('no errors if passed uuid string without dashes and converted to standart value', function() {
|
||||
$model = new UuidTestModel();
|
||||
$originalUuid = Uuid::uuid();
|
||||
$model->attribute = str_replace('-', '', $originalUuid);
|
||||
expect($model->validate())->true();
|
||||
expect($model->attribute)->equals($originalUuid);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class UuidTestModel extends Model {
|
||||
public $attribute;
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
['attribute', UuidValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
@ -9,4 +9,7 @@ return [
|
||||
'secret' => 'private-key',
|
||||
],
|
||||
],
|
||||
'params' => [
|
||||
'authserverDomain' => 'http://authserver.ely.by',
|
||||
],
|
||||
];
|
||||
|
Loading…
Reference in New Issue
Block a user