diff --git a/autocompletion.php b/autocompletion.php index f0fb1db..0274c3c 100644 --- a/autocompletion.php +++ b/autocompletion.php @@ -9,7 +9,7 @@ class Yii extends \yii\BaseYii { /** - * @var BaseApplication|WebApplication|ConsoleApplication the application instance + * @var BaseApplication|WebApplication|\app\components\ConsoleApplication the application instance */ public static $app; } @@ -32,14 +32,6 @@ class WebApplication extends yii\web\Application { } -/** - * Class ConsoleApplication - * Include only Console application related components here - */ -class ConsoleApplication extends yii\console\Application -{ -} - /** * User component * Include only Web application related components here diff --git a/commands/CoreServerController.php b/commands/ServerController.php similarity index 100% rename from commands/CoreServerController.php rename to commands/ServerController.php diff --git a/components/ConsoleApplication.php b/components/ConsoleApplication.php new file mode 100644 index 0000000..f379fcd --- /dev/null +++ b/components/ConsoleApplication.php @@ -0,0 +1,26 @@ +eventManager->registerModulesHandlers($this->modules); + } +} diff --git a/components/EventManager.php b/components/EventManager.php new file mode 100644 index 0000000..adccb33 --- /dev/null +++ b/components/EventManager.php @@ -0,0 +1,59 @@ +trigger($name, $event); + } + + /** + * @param array $handlers + * @throws InvalidConfigException + */ + public function registerHandlers($handlers) + { + foreach ($handlers as $handler) { + if (count($handler) === 2) { + \Yii::$app->on($handler[0], $handler[1]); + } elseif (count($handler) === 3) { + Event::on($handler[0], $handler[1], $handler[2]); + } else { + throw new InvalidConfigException('Invalid event configuration'); + } + } + } + + /** + * @param array $modules + */ + public function registerModulesHandlers($modules) + { + foreach ($modules as $module) { + if (!is_array($module)) { + continue; + } + + /** @var BaseModule $moduleClass */ + $moduleClass = $module['class']; + + if (method_exists($moduleClass, 'getEventHandlers')) { + $eventHandlers = $moduleClass::getEventHandlers(); + + $this->registerHandlers($eventHandlers); + } + } + } +} diff --git a/config/console.php b/config/console.php index 7306e27..1f047f1 100644 --- a/config/console.php +++ b/config/console.php @@ -22,9 +22,13 @@ $config = [ ], ], ], + 'eventManager' => [ + 'class' => 'app\components\EventManager', + ], 'db' => $db, ], 'params' => $params, + 'modules' => require 'modules.php', /* 'controllerMap' => [ 'fixture' => [ // Fixture generation command line. diff --git a/config/main.php b/config/main.php index 6592e7f..a226125 100644 --- a/config/main.php +++ b/config/main.php @@ -70,17 +70,7 @@ return [ ], ], ], - 'modules' => [ - 'admin' => [ - 'class' => 'app\modules\admin\Module', - ], - 'api' => [ - 'class' => 'app\modules\api\Module', - ], - 'datecontrol' => [ - 'class' => '\kartik\datecontrol\Module' - ], - ], + 'modules' => require 'modules.php', 'params' => $params, 'defaultRoute' => ['panel/index'], ]; diff --git a/config/modules.php b/config/modules.php new file mode 100644 index 0000000..fb649a8 --- /dev/null +++ b/config/modules.php @@ -0,0 +1,19 @@ + [ + 'class' => 'app\modules\admin\Module', + ], + 'api' => [ + 'class' => 'app\modules\api\Module', + ], + 'datecontrol' => [ + 'class' => '\kartik\datecontrol\Module' + ], + 'customModule' => [ + 'class' => 'app\modules\customModule\Module', + ], + 'server' => [ + 'class' => 'app\modules\server\Module', + ], +]; diff --git a/migrations/m170901_134923_create_device.php b/migrations/m170901_134923_create_device.php new file mode 100644 index 0000000..416e701 --- /dev/null +++ b/migrations/m170901_134923_create_device.php @@ -0,0 +1,52 @@ +db->driverName === 'mysql') { + $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; + } + + $this->createTable('device', [ + 'id' => $this->primaryKey(), + 'type' => $this->string(100)->notNull(), + 'room_id' => $this->integer(), + 'name' => $this->string(100)->notNull(), + 'title' => $this->string()->notNull(), + 'key' => $this->string()->notNull(), + 'allow_remote_connection' => $this->boolean()->notNull()->defaultValue(false), + ], $tableOptions); + + $this->createIndex('idx-device-type', 'device', 'type'); + $this->createIndex('idx-device-room_id', 'device', 'room_id'); + $this->createIndex('idx-device-key', 'device', 'key', true); + + $this->addForeignKey( + 'fk-device-room_id', + 'device', + 'room_id', + 'room', + 'id', + 'SET NULL' + ); + } + + public function safeDown() + { + $this->dropForeignKey( + 'fk-device-room_id', + 'device' + ); + + $this->dropIndex('idx-device-type', 'device'); + $this->dropIndex('idx-device-room_id', 'device'); + $this->dropIndex('idx-device-key', 'device'); + + $this->dropTable('device'); + } +} diff --git a/models/Device.php b/models/Device.php new file mode 100644 index 0000000..146d87b --- /dev/null +++ b/models/Device.php @@ -0,0 +1,77 @@ + 100], + [['title', 'key'], 'string', 'max' => 255], + [['key'], 'unique'], + [['room_id'], 'exist', 'skipOnError' => true, 'targetClass' => Room::className(), 'targetAttribute' => ['room_id' => 'id']], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'id' => 'ID', + 'type' => 'Type', + 'room_id' => 'Room ID', + 'name' => 'Name', + 'title' => 'Title', + 'key' => 'Key', + 'allow_remote_connection' => 'Allow Remote Connection', + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getRoom() + { + return $this->hasOne(Room::className(), ['id' => 'room_id'])->inverseOf('devices'); + } + + /** + * @inheritdoc + * @return \app\models\query\DeviceQuery the active query used by this AR class. + */ + public static function find() + { + return new \app\models\query\DeviceQuery(get_called_class()); + } +} diff --git a/models/query/DeviceQuery.php b/models/query/DeviceQuery.php new file mode 100644 index 0000000..9ab4b68 --- /dev/null +++ b/models/query/DeviceQuery.php @@ -0,0 +1,34 @@ +andWhere('[[status]]=1'); + }*/ + + /** + * @inheritdoc + * @return \app\models\Device[]|array + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * @inheritdoc + * @return \app\models\Device|array|null + */ + public function one($db = null) + { + return parent::one($db); + } +} diff --git a/modules/BaseModule.php b/modules/BaseModule.php new file mode 100644 index 0000000..ad8e705 --- /dev/null +++ b/modules/BaseModule.php @@ -0,0 +1,16 @@ +connection->resourceId . ': ' . $event->message . PHP_EOL; + + $event->server->sendAllUsers([ + 'message', + [ + 'from_id' => $event->connection->resourceId, + 'text' => $event->message, + ], + ]); + } +} diff --git a/modules/customModule/Module.php b/modules/customModule/Module.php new file mode 100644 index 0000000..5e5438d --- /dev/null +++ b/modules/customModule/Module.php @@ -0,0 +1,44 @@ +controllerNamespace = 'app\modules\server\commands'; + } + } +} diff --git a/modules/server/commands/StartController.php b/modules/server/commands/StartController.php new file mode 100644 index 0000000..13cb73b --- /dev/null +++ b/modules/server/commands/StartController.php @@ -0,0 +1,38 @@ +listen($port, '0.0.0.0'); + + $server = new IoServer( + new HttpServer( + new WsServer( + new CoreServer([ + 'loop' => $loop, + ]) + ) + ), + $socket, + $loop + ); + + $server->run(); + } +} diff --git a/modules/server/components/BaseServer.php b/modules/server/components/BaseServer.php new file mode 100644 index 0000000..c03c8c6 --- /dev/null +++ b/modules/server/components/BaseServer.php @@ -0,0 +1,29 @@ +eventManager->trigger($name, $event); + } + +} diff --git a/modules/server/components/CoreServer.php b/modules/server/components/CoreServer.php new file mode 100644 index 0000000..0621ae1 --- /dev/null +++ b/modules/server/components/CoreServer.php @@ -0,0 +1,328 @@ +trigger(self::EVENT_INIT, new ServerEvent([ + 'server' => $this, + ])); + } + + /** + * @param string|array|object $data + */ + public function sendAllUsers($data) + { + if (is_array($data) || is_object($data)) { + $msg = Json::encode($data); + } else { + $msg = $data; + } + } + + /** + * @param string|array|object $data + */ + public function sendAllDevices($data) + { + + foreach ($this->deviceClients as $client) { + $client->send($msg); + } + } + + /** + * When a new connection is opened it will be passed to this method + * @param ConnectionInterface $connection The socket/connection that just connected to your application + * @throws \Exception + */ + public function onOpen(ConnectionInterface $connection) + { + if (!$this->authenticate($connection)) { + return; + } + + $this->trigger(self::EVENT_CONNECTION_OPEN, new ConnectionEvent([ + 'server' => $this, + 'connection' => $connection, + ])); + } + + /** + * This is called before or after a socket is closed (depends on how it's closed). SendMessage to $conn will not result in an error if it has already been closed. + * @param ConnectionInterface $conn The socket/connection that is closing/closed + * @throws \Exception + */ + public function onClose(ConnectionInterface $conn) + { + if (isset($this->clients[$conn->resourceId])) { + unset($this->clients[$conn->resourceId]); + } + + $this->trigger(self::EVENT_CONNECTION_CLOSE, new ConnectionEvent([ + 'server' => $this, + 'connection' => $conn, + ])); + } + + /** + * If there is an error with one of the sockets, or somewhere in the application where an Exception is thrown, + * the Exception is sent back down the stack, handled by the Server and bubbled back up the application through this method + * @param ConnectionInterface $conn + * @param \Exception $e + * @throws \Exception + */ + public function onError(ConnectionInterface $conn, \Exception $e) + { + $this->trigger(self::EVENT_CONNECTION_ERROR, new ConnectionErrorEvent([ + 'server' => $this, + 'connection' => $conn, + 'exception' => $e, + ])); + } + + /** + * Triggered when a client sends data through the socket + * @param \Ratchet\ConnectionInterface $from The socket/connection that sent the message to your application + * @param string $msg The message received + * @throws \Exception + */ + public function onMessage(ConnectionInterface $from, $msg) + { + $this->trigger(self::EVENT_CONNECTION_MESSAGE, new ConnectionMessageEvent([ + 'server' => $this, + 'connection' => $from, + 'message' => $msg, + ])); + } + + /** + * @param Connection $connection + */ + protected function authenticate(Connection $connection) + { + /** @var QueryString $query */ + $query = $connection->WebSocket->request->getQuery(); + + $type = $query->get('type'); + + switch ($type) { + case 'user': + $this->authUser($connection, $query); + break; + case 'device': + $this->authDevice($connection, $query); + break; + default: + throw new InvalidParamException('Unknown connection type'); + } + } + + /** + * @param Connection $connection + * @param QueryString $query + */ + protected function authUser(Connection $connection, QueryString $query) + { + $authToken = (string)$query->get('auth_token'); + $apiKey = (string)$query->get('api_key'); + + if (!$authToken && !$apiKey) { + $connection->close(); + + throw new InvalidParamException('No token or API key provided'); + } + + $ip = $connection->remoteAddress; + $origin = $connection->WebSocket->request->getHeader('Origin'); + + $isInternal = $apiKey !== null && $ip === '127.0.0.1' && $origin === 'api'; + + if ($isInternal) { + $user = User::findOne([ + 'api_key' => $apiKey, + ]); + } else { + $user = User::findOne([ + 'auth_token' => $authToken, + ]); + } + + if (!$user) { + $connection->close(); + + $this->trigger(self::EVENT_AUTH_ERROR, new UserAuthEvent([ + 'connection' => $connection, + 'server' => $this, + 'user' => $user, + 'data' => [ + 'authToken' => $authToken, + 'apiKey' => $apiKey, + 'ip' => $ip, + 'origin' => $origin, + ], + ])); + + return; + } + + $user->reGenerateAuthToken(); + + $connection->isInternal = $isInternal; + $connection->user = $user; + + $this->userClients[$user->id] = []; + $this->userClients[$user->id][$connection->resourceId] = $connection; + + $this->trigger(self::EVENT_AUTH_USER, new UserAuthEvent([ + 'connection' => $connection, + 'server' => $this, + 'user' => $user, + 'data' => [ + 'authToken' => $authToken, + 'apiKey' => $apiKey, + 'ip' => $ip, + 'origin' => $origin, + ], + ])); + } + + /** + * @param Connection $connection + * @param QueryString $query + */ + protected function authDevice(Connection $connection, QueryString $query) + { + $deviceKey = (string)$query->get('key'); + + $isLocal = $connection->remoteAddress === '127.0.0.1'; + + if (!$deviceKey) { + $connection->close(); + + throw new InvalidParamException('No device key provided'); + } + + $device = Device::findOne([ + 'key' => $deviceKey, + ]); + + if (!$device || (!$isLocal && !$device->allow_remote_connection)) { + $connection->close(); + + $this->trigger(self::EVENT_AUTH_ERROR, new DeviceAuthEvent([ + 'connection' => $connection, + 'server' => $this, + 'device' => $device, + 'data' => [ + 'authToken' => $deviceKey, + 'apiKey' => $isLocal, + 'ip' => $connection->remoteAddress, + ], + ])); + } + + $this->deviceClients[$device->id] = $connection; + + $this->trigger(self::EVENT_AUTH_USER, new DeviceAuthEvent([ + 'connection' => $connection, + 'server' => $this, + 'device' => $device, + 'data' => [ + 'authToken' => $deviceKey, + 'apiKey' => $isLocal, + 'ip' => $connection->remoteAddress, + ], + ])); + } + + /** + * @param object|array|string $data + */ + public function sendUsers($data) + { + $msg = $this->encodeSendData($data); + + foreach ($this->userClients as $client) { + /** @var Connection $connection */ + foreach ($client as $connection) { + $connection->send($msg); + } + } + } + + /** + * @param int $userId + * @param object|array|string $data + */ + public function sendUser(int $userId, $data) + { + $msg = $this->encodeSendData($data); + + if (isset($this->userClients[$userId]) && count($this->userClients[$userId]) > 0) { + /** @var Connection $connection */ + foreach ($this->userClients[$userId] as $connection) { + $connection->send($msg); + } + } + } + + /** + * @param object|array|string $data + * @return string + */ + public function encodeSendData($data) + { + $msg = $data; + + if (is_array($data) || is_object($data)) { + $msg = Json::encode($data); + } + + return $msg; + } +} diff --git a/modules/server/events/ConnectionErrorEvent.php b/modules/server/events/ConnectionErrorEvent.php new file mode 100644 index 0000000..9db6700 --- /dev/null +++ b/modules/server/events/ConnectionErrorEvent.php @@ -0,0 +1,11 @@ +send(Json::encode([ + $from->send(Json::encode([ 'type' => 'error', 'message' => 'Такой элемент не существует', ])); + return; } if ($item->type !== Item::TYPE_PLANT) { - return $from->send(Json::encode([ + $from->send(Json::encode([ 'type' => 'error', 'message' => 'Данный тип устройства не является Plant', ])); + return; } $board = $item->board; @@ -648,10 +651,11 @@ class CoreServer implements MessageComponentInterface case Board::TYPE_WEBSOCKET: if (!$this->isBoardConnected($board->id)) { - return $from->send(Json::encode([ + $from->send(Json::encode([ 'type' => 'error', 'message' => 'Устройство не подключено', ])); + return; } $this->sendToBoard($board->id, [ @@ -673,8 +677,6 @@ class CoreServer implements MessageComponentInterface $this->log("Cannot log:"); var_dump($history->errors); } - - return true; } /** @@ -705,7 +707,7 @@ class CoreServer implements MessageComponentInterface $this->log("Trigger [$trigger->id] triggered by manual"); - $this->trigger($trigger); + $this->triggerTrigger($trigger); $history = new History(); $history->type = History::TYPE_API_TRIGGER; @@ -978,7 +980,7 @@ class CoreServer implements MessageComponentInterface } foreach ($triggers as $trigger) { - $this->trigger($trigger); + $this->triggerTrigger($trigger); } } @@ -1000,7 +1002,7 @@ class CoreServer implements MessageComponentInterface } foreach ($triggers as $trigger) { - $this->trigger($trigger); + $this->triggerTrigger($trigger); } } @@ -1293,7 +1295,7 @@ class CoreServer implements MessageComponentInterface */ protected function triggerDate($trigger) { - $this->trigger($trigger); + $this->triggerTrigger($trigger); } /** @@ -1301,7 +1303,7 @@ class CoreServer implements MessageComponentInterface */ protected function triggerTime($trigger) { - $this->trigger($trigger); + $this->triggerTrigger($trigger); $this->scheduleTriggers(); } @@ -1311,7 +1313,7 @@ class CoreServer implements MessageComponentInterface * * @param Trigger $trigger */ - protected function trigger($trigger) + protected function triggerTrigger($trigger) { /** @var Event[] $events */ $events = $trigger->getEvents()->andWhere(['active' => true])->all();