Merge branch 'session_server'

This commit is contained in:
ErickSkrauch 2016-09-09 00:40:12 +03:00
commit f712c07694
47 changed files with 1643 additions and 18 deletions

View File

@ -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 {

View File

@ -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);
}
}
}

View File

@ -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 {

View File

@ -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,
],
],
];

View File

@ -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',
];

View File

@ -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();
}
}
}

View File

@ -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.');
}

View 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');
}
}

View 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();
}
}

View File

@ -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);
}
}

View 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);
}
}

View 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();
}
}

View 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;
}
}

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

@ -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;
}
}

View 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);
}
}

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,14 @@
<?php
namespace api\modules\session\models\protocols;
abstract class BaseJoin implements JoinInterface {
abstract public function getAccessToken() : string;
abstract public function getSelectedProfile() : string;
abstract public function getServerId() : string;
abstract public function validate() : bool;
}

View File

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

View File

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

View File

@ -0,0 +1,63 @@
<?php
namespace api\modules\session\models\protocols;
use yii\validators\RequiredValidator;
class LegacyJoin extends BaseJoin {
private $user;
private $sessionId;
private $serverId;
private $accessToken;
private $uuid;
public function __construct(string $user, string $sessionId, string $serverId) {
$this->user = $user;
$this->sessionId = $sessionId;
$this->serverId = $serverId;
$this->parseSessionId($this->sessionId);
}
public function getAccessToken() : string {
return $this->accessToken;
}
public function getSelectedProfile() : string {
return $this->uuid ?: $this->user;
}
public function getServerId() : string {
return $this->serverId;
}
/**
* @return bool
*/
public function validate() : bool {
$validator = new RequiredValidator();
return $validator->validate($this->accessToken)
&& $validator->validate($this->user)
&& $validator->validate($this->serverId);
}
/**
* Метод проводит инициализацию значений полей для соотвествия общим канонам
* именования в проекте
*
* Бьём по ':' для учёта авторизации в современных лаунчерах и входе на более старую
* версию игры. Там sessionId передаётся как "token:{accessToken}:{uuid}", так что это нужно обработать
*/
protected function parseSessionId(string $sessionId) {
$parts = explode(':', $sessionId);
if (count($parts) === 3) {
$this->accessToken = $parts[1];
$this->uuid = $parts[2];
} else {
$this->accessToken = $this->sessionId;
}
}
}

View File

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

View File

@ -0,0 +1,38 @@
<?php
namespace api\modules\session\models\protocols;
use yii\validators\RequiredValidator;
class ModernJoin extends BaseJoin {
private $accessToken;
private $selectedProfile;
private $serverId;
public function __construct(string $accessToken, string $selectedProfile, string $serverId) {
$this->accessToken = $accessToken;
$this->selectedProfile = $selectedProfile;
$this->serverId = $serverId;
}
public function getAccessToken() : string {
return $this->accessToken;
}
public function getSelectedProfile() : string {
return $this->selectedProfile;
}
public function getServerId() : string {
return $this->serverId;
}
public function validate() : bool {
$validator = new RequiredValidator();
return $validator->validate($this->accessToken)
&& $validator->validate($this->selectedProfile)
&& $validator->validate($this->serverId);
}
}

View File

@ -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;
}
}

View File

@ -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 {
}

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

@ -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',

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -39,7 +39,7 @@ class OauthAccessToken extends ActiveRecord {
return true;
}
public function isExpired() {
public function isExpired() : bool {
return time() > $this->expire_time;
}

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

@ -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, []);
}
}
}

View File

@ -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

View File

@ -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;

View 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());
}
}

View File

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

View File

@ -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);
}
}

View File

@ -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.',
]);
}
}

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');
}
}

View 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',
]);
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace tests\codeception\api\functional\sessionserver;
use common\models\OauthScope as S;
use Faker\Provider\Uuid;
use tests\codeception\api\_pages\SessionServerRoute;
use tests\codeception\api\functional\_steps\AuthserverSteps;
use tests\codeception\api\functional\_steps\OauthSteps;
use tests\codeception\api\FunctionalTester;
class JoinLegacyCest {
/**
* @var SessionServerRoute
*/
private $route;
public function _before(AuthserverSteps $I) {
$this->route = new SessionServerRoute($I);
}
public function joinByLegacyAuthserver(AuthserverSteps $I) {
$I->wantTo('join to server by legacy protocol, using legacy authserver access token');
list($accessToken) = $I->amAuthenticated();
$this->route->joinLegacy([
'sessionId' => $accessToken,
'user' => 'Admin',
'serverId' => Uuid::uuid(),
]);
$this->expectSuccessResponse($I);
}
public function joinByNewSessionFormat(AuthserverSteps $I) {
$I->wantTo('join to server by legacy protocol with new launcher session format, using legacy authserver');
list($accessToken) = $I->amAuthenticated();
$this->route->joinLegacy([
'sessionId' => 'token:' . $accessToken . ':' . 'df936908-b2e1-544d-96f8-2977ec213022',
'user' => 'Admin',
'serverId' => Uuid::uuid(),
]);
$this->expectSuccessResponse($I);
}
public function joinByOauth2Token(OauthSteps $I) {
$I->wantTo('join to server using modern oAuth2 generated token with new launcher session format');
$accessToken = $I->getAccessToken([S::MINECRAFT_SERVER_SESSION]);
$this->route->joinLegacy([
'sessionId' => 'token:' . $accessToken . ':' . 'df936908-b2e1-544d-96f8-2977ec213022',
'user' => 'Admin',
'serverId' => Uuid::uuid(),
]);
$this->expectSuccessResponse($I);
}
public function wrongArguments(FunctionalTester $I) {
$I->wantTo('get error on wrong amount of arguments');
$this->route->joinLegacy([
'wrong' => 'argument',
]);
$I->canSeeResponseCodeIs(400);
$I->canSeeResponseContains('credentials can not be null.');
}
public function joinWithWrongAccessToken(FunctionalTester $I) {
$I->wantTo('join to some server with wrong accessToken');
$this->route->joinLegacy([
'sessionId' => 'token:' . Uuid::uuid() . ':' . Uuid::uuid(),
'user' => 'random-username',
'serverId' => Uuid::uuid(),
]);
$I->seeResponseCodeIs(401);
$I->canSeeResponseContains('Ely.by authorization required');
}
public function joinWithAccessTokenWithoutMinecraftPermission(OauthSteps $I) {
$I->wantTo('join to some server with wrong accessToken');
$accessToken = $I->getAccessToken([S::ACCOUNT_INFO]);
$this->route->joinLegacy([
'sessionId' => 'token:' . $accessToken . ':' . 'df936908-b2e1-544d-96f8-2977ec213022',
'user' => 'Admin',
'serverId' => Uuid::uuid(),
]);
$I->seeResponseCodeIs(401);
$I->canSeeResponseContains('Ely.by authorization required');
}
private function expectSuccessResponse(FunctionalTester $I) {
$I->seeResponseCodeIs(200);
$I->canSeeResponseEquals('OK');
}
}

View File

@ -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.',
]);
}
}

View File

@ -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);
}
}
}

View File

@ -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'));
}
}

View File

@ -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],
];
}
}

View File

@ -9,4 +9,7 @@ return [
'secret' => 'private-key',
],
],
'params' => [
'authserverDomain' => 'http://authserver.ely.by',
],
];