diff --git a/composer.json b/composer.json index 6a902e6..d5a21f8 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,8 @@ "w7corp/easywechat": "^6.8", "tencentcloud/sms": "^3.0", "ext-curl": "*", - "dh2y/think-qrcode": "^2.0" + "dh2y/think-qrcode": "^2.0", + "php-mqtt/client": "^2.0" }, "require-dev": { "symfony/var-dumper": "^4.2", diff --git a/composer.lock b/composer.lock index c53e965..181b0ca 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ef8fb5f6b7330deffd3410dd813510fc", + "content-hash": "4f45bdaca9f5395f58768a17c5a0af9f", "packages": [ { "name": "adbario/php-dot-notation", @@ -1730,6 +1730,63 @@ ], "time": "2023-01-10T14:29:55+00:00" }, + { + "name": "php-mqtt/client", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-mqtt/client.git", + "reference": "458afc0bf33075ed8a1ffad72af5e10f2b516220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mqtt/client/zipball/458afc0bf33075ed8a1ffad72af5e10f2b516220", + "reference": "458afc0bf33075ed8a1ffad72af5e10f2b516220", + "shasum": "" + }, + "require": { + "myclabs/php-enum": "^1.7", + "php": "^8.0", + "psr/log": "^1.1|^2.0|^3.0" + }, + "require-dev": { + "phpunit/php-invoker": "^3.0", + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-redis": "Required for the RedisRepository" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpMqtt\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marvin Mall", + "email": "marvin-mall@msn.com", + "role": "developer" + } + ], + "description": "An MQTT client written in and for PHP.", + "keywords": [ + "client", + "mqtt", + "publish", + "subscribe" + ], + "support": { + "issues": "https://github.com/php-mqtt/client/issues", + "source": "https://github.com/php-mqtt/client/tree/v2.0.0" + }, + "time": "2023-11-25T20:53:47+00:00" + }, { "name": "phpoffice/phpspreadsheet", "version": "1.28.0", @@ -4961,5 +5018,5 @@ "ext-curl": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index 254ee3b..e297e25 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -46,6 +46,7 @@ return array( 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), 'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'), + 'PhpMqtt\\Client\\' => array($vendorDir . '/php-mqtt/client/src'), 'Overtrue\\Socialite\\' => array($vendorDir . '/overtrue/socialite/src'), 'OSS\\' => array($vendorDir . '/aliyuncs/oss-sdk-php/src/OSS'), 'Nyholm\\Psr7\\' => array($vendorDir . '/nyholm/psr7/src'), diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 7343a84..6573de6 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -104,6 +104,7 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e 'Psr\\Container\\' => 14, 'Psr\\Cache\\' => 10, 'PhpOffice\\PhpSpreadsheet\\' => 25, + 'PhpMqtt\\Client\\' => 15, ), 'O' => array ( @@ -316,6 +317,10 @@ class ComposerStaticInitd2a74ba94e266cc4f45a64c54a292d7e array ( 0 => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet', ), + 'PhpMqtt\\Client\\' => + array ( + 0 => __DIR__ . '/..' . '/php-mqtt/client/src', + ), 'Overtrue\\Socialite\\' => array ( 0 => __DIR__ . '/..' . '/overtrue/socialite/src', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 82681ad..4d7618a 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -1790,6 +1790,66 @@ ], "install-path": "../overtrue/socialite" }, + { + "name": "php-mqtt/client", + "version": "v2.0.0", + "version_normalized": "2.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-mqtt/client.git", + "reference": "458afc0bf33075ed8a1ffad72af5e10f2b516220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mqtt/client/zipball/458afc0bf33075ed8a1ffad72af5e10f2b516220", + "reference": "458afc0bf33075ed8a1ffad72af5e10f2b516220", + "shasum": "" + }, + "require": { + "myclabs/php-enum": "^1.7", + "php": "^8.0", + "psr/log": "^1.1|^2.0|^3.0" + }, + "require-dev": { + "phpunit/php-invoker": "^3.0", + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-redis": "Required for the RedisRepository" + }, + "time": "2023-11-25T20:53:47+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "PhpMqtt\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marvin Mall", + "email": "marvin-mall@msn.com", + "role": "developer" + } + ], + "description": "An MQTT client written in and for PHP.", + "keywords": [ + "client", + "mqtt", + "publish", + "subscribe" + ], + "support": { + "issues": "https://github.com/php-mqtt/client/issues", + "source": "https://github.com/php-mqtt/client/tree/v2.0.0" + }, + "install-path": "../php-mqtt/client" + }, { "name": "phpoffice/phpspreadsheet", "version": "1.28.0", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index a25a087..60b0298 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'topthink/think', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '68951aabafc2dea38eda4ad270d23540f05b1e62', + 'reference' => 'f3cff15dd91e4ec87d9ef4e7d8c466a239a3d08b', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -232,6 +232,15 @@ 0 => '1.0', ), ), + 'php-mqtt/client' => array( + 'pretty_version' => 'v2.0.0', + 'version' => '2.0.0.0', + 'reference' => '458afc0bf33075ed8a1ffad72af5e10f2b516220', + 'type' => 'library', + 'install_path' => __DIR__ . '/../php-mqtt/client', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'phpoffice/phpspreadsheet' => array( 'pretty_version' => '1.28.0', 'version' => '1.28.0.0', @@ -607,7 +616,7 @@ 'topthink/think' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '68951aabafc2dea38eda4ad270d23540f05b1e62', + 'reference' => 'f3cff15dd91e4ec87d9ef4e7d8c466a239a3d08b', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/vendor/php-mqtt/client/.ci/emqx.conf b/vendor/php-mqtt/client/.ci/emqx.conf new file mode 100644 index 0000000..88e3c7b --- /dev/null +++ b/vendor/php-mqtt/client/.ci/emqx.conf @@ -0,0 +1,2503 @@ +## EMQX Configuration + +## NOTE: Do not change format of CONFIG_SECTION_{BGN,END} comments! + +## CONFIG_SECTION_BGN=cluster ================================================== + +## Cluster name. +## +## Value: String +cluster.name = emqxcl + +## Specify the erlang distributed protocol. +## +## Value: Enum +## - inet_tcp: the default; handles TCP streams with IPv4 addressing. +## - inet6_tcp: handles TCP with IPv6 addressing. +## - inet_tls: using TLS for Erlang Distribution. +## +## vm.args: -proto_dist inet_tcp +cluster.proto_dist = inet_tcp + +## Cluster auto-discovery strategy. +## +## Value: Enum +## - manual: Manual join command +## - static: Static node list +## - mcast: IP Multicast +## - dns: DNS A Record +## - etcd: etcd +## - k8s: Kubernetes +## +## Default: manual +cluster.discovery = manual + +## Enable cluster autoheal from network partition. +## +## Value: on | off +## +## Default: on +cluster.autoheal = on + +## Autoclean down node. A down node will be removed from the cluster +## if this value > 0. +## +## Value: Duration +## -h: hour, e.g. '2h' for 2 hours +## -m: minute, e.g. '5m' for 5 minutes +## -s: second, e.g. '30s' for 30 seconds +## +## Default: 5m +cluster.autoclean = 5m + +##-------------------------------------------------------------------- +## Cluster using static node list + +## Node list of the cluster. +## +## Value: String +## cluster.static.seeds = emqx1@127.0.0.1,emqx2@127.0.0.1 + +##-------------------------------------------------------------------- +## Cluster using IP Multicast. + +## IP Multicast Address. +## +## Value: IP Address +## cluster.mcast.addr = 239.192.0.1 + +## Multicast Ports. +## +## Value: Port List +## cluster.mcast.ports = 4369,4370 + +## Multicast Iface. +## +## Value: Iface Address +## +## Default: 0.0.0.0 +## cluster.mcast.iface = 0.0.0.0 + +## Multicast Ttl. +## +## Value: 0-255 +## cluster.mcast.ttl = 255 + +## Multicast loop. +## +## Value: on | off +## cluster.mcast.loop = on + +##-------------------------------------------------------------------- +## Cluster using DNS A records. + +## DNS name. +## +## Value: String +## cluster.dns.name = localhost + +## The App name is used to build 'node.name' with IP address. +## +## Value: String +## cluster.dns.app = emqx + +## Type of dns record. +## +## Value: Value: a | srv +## cluster.dns.type = a + +##-------------------------------------------------------------------- +## Cluster using etcd + +## Etcd server list, seperated by ','. +## +## Value: String +## cluster.etcd.server = http://127.0.0.1:2379 + +## The prefix helps build nodes path in etcd. Each node in the cluster +## will create a path in etcd: v2/keys/// +## +## Value: String +## cluster.etcd.prefix = emqxcl + +## The TTL for node's path in etcd. +## +## Value: Duration +## +## Default: 1m, 1 minute +## cluster.etcd.node_ttl = 1m + +## Path to a file containing the client's private PEM-encoded key. +## +## Value: File +## cluster.etcd.ssl.keyfile = etc/certs/client-key.pem + +## The path to a file containing the client's certificate. +## +## Value: File +## cluster.etcd.ssl.certfile = etc/certs/client.pem + +## Path to the file containing PEM-encoded CA certificates. The CA certificates +## are used during server authentication and when building the client certificate chain. +## +## Value: File +## cluster.etcd.ssl.cacertfile = etc/certs/ca.pem + +##-------------------------------------------------------------------- +## Cluster using Kubernetes + +## Kubernetes API server list, seperated by ','. +## +## Value: String +## cluster.k8s.apiserver = http://10.110.111.204:8080 + +## The service name helps lookup EMQ nodes in the cluster. +## +## Value: String +## cluster.k8s.service_name = emqx + +## The address type is used to extract host from k8s service. +## +## Value: ip | dns | hostname +## cluster.k8s.address_type = ip + +## The app name helps build 'node.name'. +## +## Value: String +## cluster.k8s.app_name = emqx + +## The suffix added to dns and hostname get from k8s service +## +## Value: String +## cluster.k8s.suffix = pod.cluster.local + +## Kubernetes Namespace +## +## Value: String +## cluster.k8s.namespace = default + +## CONFIG_SECTION_END=cluster ================================================== + +##-------------------------------------------------------------------- +## Node +##-------------------------------------------------------------------- + +## Node name. +## +## See: http://erlang.org/doc/reference_manual/distributed.html +## +## Value: @ +## +## Default: emqx@127.0.0.1 +node.name = emqx@127.0.0.1 + +## Cookie for distributed node communication. +## +## Value: String +node.cookie = emqxsecretcookie + +## Data dir for the node +## +## Value: Folder +node.data_dir = data + +## Heartbeat monitoring of an Erlang runtime system. Comment the line to disable +## heartbeat, or set the value as 'on' +## +## Turning this on may cause the node to restart if it becomes unresponsive to +## the heartbeat pings. +## +## NOTE: When managed by systemd (or other supervision tools like systemd), +## heart will probably only cause EMQX to stop, but restart or not will +## depend on systemd's restart strategy. +## NOTE: When running in docker, the container will die as soon as the the +## heart process kills EMQX, but restart or not will depend on container +## supervision strategy, such as k8s restartPolicy. +## +## Value: on +## +## vm.args: -heart +## node.heartbeat = on + +## Sets the number of threads in async thread pool. Valid range is 0-1024. +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: 0-1024 +## +## vm.args: +A Number +## node.async_threads = 4 + +## Sets the maximum number of simultaneously existing processes for this +## system if a Number is passed as value. +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: Number [1024-134217727] +## +## vm.args: +P Number +## node.process_limit = 2097152 + +## Sets the maximum number of simultaneously existing ports for this system. +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: Number [1024-134217727] +## +## vm.args: +Q Number +## node.max_ports = 1048576 + +## Sets the distribution buffer busy limit (dist_buf_busy_limit). +## +## See: http://erlang.org/doc/man/erl.html +## +## Value: Number [1KB-2GB] +## +## vm.args: +zdbbl size +## node.dist_buffer_size = 8MB + +## Sets the maximum number of ETS tables. Note that mnesia and SSL will +## create temporary ETS tables. +## +## Value: Number +## +## vm.args: +e Number +## node.max_ets_tables = 262144 + +## Global GC Interval. +## +## Value: Duration +## +## Examples: +## - 2h: 2 hours +## - 30m: 30 minutes +## - 20s: 20 seconds +## +## Defaut: 15 minutes +node.global_gc_interval = 15m + +## Tweak GC to run more often. +## +## Value: Number [0-65535] +## +## vm.args: -env ERL_FULLSWEEP_AFTER Number +## node.fullsweep_after = 1000 + +## Crash dump log file. +## +## Value: Log file +node.crash_dump = log/crash.dump + +## Specify SSL Options in the file if using SSL for Erlang Distribution. +## +## Value: File +## +## vm.args: -ssl_dist_optfile +## node.ssl_dist_optfile = etc/ssl_dist.conf + +## Sets the net_kernel tick time. TickTime is specified in seconds. +## Notice that all communicating nodes are to have the same TickTime +## value specified. +## +## See: http://www.erlang.org/doc/man/kernel_app.html#net_ticktime +## +## Value: Number +## +## vm.args: -kernel net_ticktime Number +## node.dist_net_ticktime = 120 + +## Sets the port range for the listener socket of a distributed Erlang node. +## Note that if there are firewalls between clustered nodes, this port segment +## for nodes’ communication should be allowed. +## +## See: http://www.erlang.org/doc/man/kernel_app.html +## +## Value: Port [1024-65535] +node.dist_listen_min = 6369 +node.dist_listen_max = 6369 + +node.backtrace_depth = 16 + +## CONFIG_SECTION_BGN=rpc ====================================================== + +## RPC Mode. +## +## Value: sync | async +rpc.mode = async + +## Max batch size of async RPC requests. +## +## Value: Integer +## Zero or negative value disables rpc batching. +## +## NOTE: RPC batch won't work when rpc.mode = sync +rpc.async_batch_size = 256 + +## RPC port discovery +## +## The strategy for discovering the RPC listening port of other nodes. +## +## Value: Enum +## - manual: discover ports by `tcp_server_port` and `tcp_client_port`. +## - stateless: discover ports in a stateless manner. +## If node name is `emqx@127.0.0.1`, where the `` is an integer, +## then the listening port will be `5370 + ` +## +## Defaults to `stateless`. +rpc.port_discovery = stateless + +## TCP port number for RPC server to listen on. +## +## Only takes effect when `rpc.port_discovery` = `manual`. +## +## NOTE: All nodes in the cluster should agree to this same config. +## +## Value: Port [1024-65535] +#rpc.tcp_server_port = 5369 + +## Number of outgoing RPC connections. +## +## Value: Interger [0-256] +## Default = 1 +#rpc.tcp_client_num = 0 + +## RCP Client connect timeout. +## +## Value: Seconds +rpc.connect_timeout = 5s + +## TCP send timeout of RPC client and server. +## +## Value: Seconds +rpc.send_timeout = 5s + +## Authentication timeout +## +## Value: Seconds +rpc.authentication_timeout = 5s + +## Default receive timeout for call() functions +## +## Value: Seconds +rpc.call_receive_timeout = 15s + +## Socket idle keepalive. +## +## Value: Seconds +rpc.socket_keepalive_idle = 900s + +## TCP Keepalive probes interval. +## +## Value: Seconds +rpc.socket_keepalive_interval = 75s + +## Probes lost to close the connection +## +## Value: Integer +rpc.socket_keepalive_count = 9 + +## Size of TCP send buffer. +## +## Value: Bytes +rpc.socket_sndbuf = 1MB + +## Size of TCP receive buffer. +## +## Value: Seconds +rpc.socket_recbuf = 1MB + +## Size of user-level software socket buffer. +## +## Value: Seconds +rpc.socket_buffer = 1MB + +## CONFIG_SECTION_END=rpc ====================================================== + +## CONFIG_SECTION_BGN=logger =================================================== + +## Where to emit the logs. +## Enable the console (standard output) logs. +## +## Value: file | console | both +## - file: write logs only to file +## - console: write logs only to standard I/O +## - both: write logs both to file and standard I/O +log.to = file + +## The log severity level. +## +## Value: debug | info | notice | warning | error | critical | alert | emergency +## +## Note: Only the messages with severity level higher than or equal to +## this level will be logged. +## +## Default: warning +log.level = warning + +## The dir for log files. +## +## Value: Folder +log.dir = log + +## The log filename for logs of level specified in "log.level". +## +## If `log.rotation` is enabled, this is the base name of the +## files. Each file in a rotated log is named .N, where N is an integer. +## +## Value: String +## Default: emqx.log +log.file = emqx.log + +## Limits the total number of characters printed for each log event. +## +## Value: Integer +## Default: No Limit +#log.chars_limit = 8192 + +## Maximum depth for Erlang term log formatting +## and Erlang process message queue inspection. +## +## Value: Integer or 'unlimited' (without quotes) +## Default: 100 +#log.max_depth = 100 + +## Log formatter +## Value: text | json +#log.formatter = text + +## Log to single line +## Value: Boolean +#log.single_line = true + +## Enables the log rotation. +## With this enabled, new log files will be created when the current +## log file is full, max to `log.rotation.size` files will be created. +## +## Value: on | off +## Default: on +log.rotation = on + +## Maximum size of each log file. +## +## Value: Number +## Default: 10M +## Supported Unit: KB | MB | GB +log.rotation.size = 10MB + +## Maximum rotation count of log files. +## +## Value: Number +## Default: 5 +log.rotation.count = 5 + +## To create additional log files for specific log levels. +## +## Value: File Name +## Format: log.$level.file = $filename, +## where "$level" can be one of: debug, info, notice, warning, +## error, critical, alert, emergency +## Note: Log files for a specific log level will only contain all the logs +## that higher than or equal to that level +## +#log.info.file = info.log +#log.error.file = error.log + +## The max allowed queue length before switching to sync mode. +## +## Log overload protection parameter. If the message queue grows +## larger than this value the handler switches from anync to sync mode. +## +## Default: 100 +## +#log.sync_mode_qlen = 100 + +## The max allowed queue length before switching to drop mode. +## +## Log overload protection parameter. When the message queue grows +## larger than this threshold, the handler switches to a mode in which +## it drops all new events that senders want to log. +## +## Default: 3000 +## +#log.drop_mode_qlen = 3000 + +## The max allowed queue length before switching to flush mode. +## +## Log overload protection parameter. If the length of the message queue +## grows larger than this threshold, a flush (delete) operation takes place. +## To flush events, the handler discards the messages in the message queue +## by receiving them in a loop without logging. +## +## Default: 8000 +## +#log.flush_qlen = 8000 + +## Kill the log handler when it gets overloaded. +## +## Log overload protection parameter. It is possible that a handler, +## even if it can successfully manage peaks of high load without crashing, +## can build up a large message queue, or use a large amount of memory. +## We could kill the log handler in these cases and restart it after a +## few seconds. +## +## Default: on +## +#log.overload_kill = on + +## The max allowed queue length before killing the log hanlder. +## +## Log overload protection parameter. This is the maximum allowed queue +## length. If the message queue grows larger than this, the handler +## process is terminated. +## +## Default: 20000 +## +#log.overload_kill_qlen = 20000 + +## The max allowed memory size before killing the log hanlder. +## +## Log overload protection parameter. This is the maximum memory size +## that the handler process is allowed to use. If the handler grows +## larger than this, the process is terminated. +## +## Default: 30MB +## +#log.overload_kill_mem_size = 30MB + +## Restart the log hanlder after some seconds. +## +## Log overload protection parameter. If the handler is terminated, +## it restarts automatically after a delay specified in seconds. +## The value "infinity" prevents restarts. +## +## Default: 5s +## +#log.overload_kill_restart_after = 5s + +## Max burst count and time window for burst control. +## +## Log overload protection parameter. Large bursts of log events - many +## events received by the handler under a short period of time - can +## potentially cause problems. By specifying the maximum number of events +## to be handled within a certain time frame, the handler can avoid +## choking the log with massive amounts of printouts. +## +## This config controls the maximum number of events to handle within +## a time frame. After the limit is reached, successive events are +## dropped until the end of the time frame. +## +## Note that there would be no warning if any messages were +## dropped because of burst control. +## +## Comment this config out to disable the burst control feature. +## +## Value: MaxBurstCount,TimeWindow +## Default: disabled +## +#log.burst_limit = 20000, 1s + +## CONFIG_SECTION_END=logger =================================================== + +##-------------------------------------------------------------------- +## Authentication/Access Control +##-------------------------------------------------------------------- + +## Allow anonymous authentication by default if no auth plugins loaded. +## Notice: Disable the option in production deployment! +## +## Value: true | false +allow_anonymous = true + +## Allow or deny if no ACL rules matched. +## +## Value: allow | deny +acl_nomatch = allow + +## Default ACL File. +## +## Value: File Name +acl_file = etc/acl.conf + +## Whether to enable ACL cache. +## +## If enabled, ACLs roles for each client will be cached in the memory +## +## Value: on | off +enable_acl_cache = on + +## The maximum count of ACL entries can be cached for a client. +## +## Value: Integer greater than 0 +## Default: 32 +acl_cache_max_size = 32 + +## The time after which an ACL cache entry will be deleted +## +## Value: Duration +## Default: 1 minute +acl_cache_ttl = 1m + +## The action when acl check reject current operation +## +## Value: ignore | disconnect +## Default: ignore +acl_deny_action = ignore + +## Specify the global flapping detect policy. +## The value is a string composed of flapping threshold, duration and banned interval. +## 1. threshold: an integer to specfify the disconnected times of a MQTT Client; +## 2. duration: the time window for flapping detect; +## 3. banned interval: the banned interval if a flapping is detected. +## +## Value: Integer,Duration,Duration +flapping_detect_policy = 30, 1m, 5m + +##-------------------------------------------------------------------- +## MQTT Protocol +##-------------------------------------------------------------------- + +## Maximum MQTT packet size allowed. +## +## Value: Bytes +## Default: 1MB +mqtt.max_packet_size = 3MB + +## Maximum length of MQTT clientId allowed. +## +## Value: Number [23-65535] +mqtt.max_clientid_len = 65535 + +## Maximum topic levels allowed. 0 means no limit. +## Depth so big may lead to subscribing performance issues. +## +## Value: Number [0-65535] +## Default: 128 +mqtt.max_topic_levels = 128 + +## Maximum QoS allowed. +## +## Value: 0 | 1 | 2 +mqtt.max_qos_allowed = 2 + +## Maximum Topic Alias, 0 means no topic alias supported. +## +## Value: 0-65535 +mqtt.max_topic_alias = 65535 + +## Whether the Server supports MQTT retained messages. +## +## Value: boolean +mqtt.retain_available = true + +## Whether the Server supports MQTT Wildcard Subscriptions +## +## Value: boolean +mqtt.wildcard_subscription = true + +## Whether the Server supports MQTT Shared Subscriptions. +## +## Value: boolean +mqtt.shared_subscription = true + +## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) +## +## Value: true | false +mqtt.ignore_loop_deliver = false + +## Whether to parse the MQTT frame in strict mode +## +## Value: true | false +mqtt.strict_mode = false + +## Specify the response information returned to the client +## +## Value: String +## mqtt.response_information = example + +## CONFIG_SECTION_BGN=zones =================================================== + +##-------------------------------------------------------------------- +## External Zone + +## Idle timeout of the external MQTT connections. +## +## Value: duration +zone.external.idle_timeout = 15s + +## Enable ACL check. +## +## Value: Flag +zone.external.enable_acl = on + +## Enable ban check. +## +## Value: Flag +zone.external.enable_ban = on + +## Enable per connection statistics. +## +## Value: on | off +zone.external.enable_stats = on + +## The action when acl check reject current operation +## +## Value: ignore | disconnect +## Default: ignore +zone.external.acl_deny_action = ignore + +## Force the MQTT connection process GC after this number of +## messages | bytes passed through. +## +## Numbers delimited by `|'. Zero or negative is to disable. +zone.external.force_gc_policy = 16000|16MB + +## Max message queue length and total heap size to force shutdown +## connection/session process. +## Message queue here is the Erlang process mailbox, but not the number +## of queued MQTT messages of QoS 1 and 2. +## +## Numbers delimited by `|'. Zero or negative is to disable. +## +## Default: +## - 10000|64MB on ARCH_64 system +## - 1000|32MB on ARCH_32 sytem +#zone.external.force_shutdown_policy = 10000|64MB + +## Maximum MQTT packet size allowed. +## +## Value: Bytes +## Default: 1MB +## zone.external.max_packet_size = 64KB + +## Maximum length of MQTT clientId allowed. +## +## Value: Number [23-65535] +## zone.external.max_clientid_len = 1024 + +## Maximum topic levels allowed. 0 means no limit. +## +## Value: Number [0-65535] +## zone.external.max_topic_levels = 7 + +## Maximum QoS allowed. +## +## Value: 0 | 1 | 2 +## zone.external.max_qos_allowed = 2 + +## Maximum Topic Alias, 0 means no limit. +## +## Value: 0-65535 +## zone.external.max_topic_alias = 65535 + +## Whether the Server supports retained messages. +## +## Value: boolean +## zone.external.retain_available = true + +## Whether the Server supports Wildcard Subscriptions +## +## Value: boolean +## zone.external.wildcard_subscription = false + +## Whether the Server supports Shared Subscriptions +## +## Value: boolean +## zone.external.shared_subscription = false + +## Server Keep Alive +## +## Value: Number +## zone.external.server_keepalive = 0 + +## The backoff for MQTT keepalive timeout. The broker will kick a connection out +## until 'Keepalive * backoff * 2' timeout. +## +## Value: Float > 0.5 +zone.external.keepalive_backoff = 0.75 + +## Maximum number of subscriptions allowed, 0 means no limit. +## +## Value: Number +zone.external.max_subscriptions = 0 + +## Force to upgrade QoS according to subscription. +## +## Value: on | off +zone.external.upgrade_qos = off + +## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. +## +## Value: Number +zone.external.max_inflight = 32 + +## Retry interval for QoS1/2 message delivering. +## +## Value: Duration +zone.external.retry_interval = 30s + +## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. +## +## Value: Number +zone.external.max_awaiting_rel = 100 + +## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. +## +## Value: Duration +zone.external.await_rel_timeout = 300s + +## Default session expiry interval for MQTT V3.1.1 connections. +## +## Value: Duration +## -d: day +## -h: hour +## -m: minute +## -s: second +## +## Default: 2h, 2 hours +zone.external.session_expiry_interval = 2h + +## Maximum queue length. Enqueued messages when persistent client disconnected, +## or inflight window is full. 0 means no limit. +## +## Value: Number >= 0 +zone.external.max_mqueue_len = 1000 + +## Topic priorities. +## 'none' to indicate no priority table (by default), hence all messages +## are treated equal +## +## Priority number [1-255] +## Example: topic/1=10,topic/2=8 +## NOTE: comma and equal signs are not allowed for priority topic names +## NOTE: messages for topics not in the priority table are treated as +## either highest or lowest priority depending on the configured +## value for mqueue_default_priority +## +zone.external.mqueue_priorities = none + +## Default to highest priority for topics not matching priority table +## +## Value: highest | lowest +zone.external.mqueue_default_priority = highest + +## Whether to enqueue QoS0 messages. +## +## Value: false | true +zone.external.mqueue_store_qos0 = true + +## Whether to turn on flapping detect +## +## Value: on | off +zone.external.enable_flapping_detect = off + +## Message limit for the a external MQTT connection. +## +## Value: Number,Duration +## Example: 100 messages per 10 seconds. +#zone.external.rate_limit.conn_messages_in = 100,10s + +## Bytes limit for a external MQTT connections. +## +## Value: Number,Duration +## Example: 100KB incoming per 10 seconds. +#zone.external.rate_limit.conn_bytes_in = 100KB,10s + +## Whether to alarm the congested connections. +## +## Sometimes the mqtt connection (usually an MQTT subscriber) may get "congested" because +## there're too many packets to sent. The socket trys to buffer the packets until the buffer is +## full. If more packets comes after that, the packets will be "pending" in a queue +## and we consider the connection is "congested". +## +## Enable this to send an alarm when there's any bytes pending in the queue. You could set +## the `listener.tcp..sndbuf` to a larger value if the alarm is triggered too often. +## +## The name of the alarm is of format "conn_congestion//". +## Where the is the client-id of the congested MQTT connection. +## And the is the username or "unknown_user" of not provided by the client. +## Default: off +#zone.external.conn_congestion.alarm = off + +## Won't clear the congested alarm in how long time. +## The alarm is cleared only when there're no pending bytes in the queue, and also it has been +## `min_alarm_sustain_duration` time since the last time we considered the connection is "congested". +## +## This is to avoid clearing and sending the alarm again too often. +## Default: 1m +#zone.external.conn_congestion.min_alarm_sustain_duration = 1m + +## Messages quota for the each of external MQTT connection. +## This value consumed by the number of recipient on a message. +## +## Value: Number, Duration +## +## Example: 100 messaegs per 1s +#zone.external.quota.conn_messages_routing = 100,1s + +## Messages quota for the all of external MQTT connections. +## This value consumed by the number of recipient on a message. +## +## Value: Number, Duration +## +## Example: 200000 messaegs per 1s +#zone.external.quota.overall_messages_routing = 200000,1s + +## All the topics will be prefixed with the mountpoint path if this option is enabled. +## +## Variables in mountpoint path: +## - %c: clientid +## - %u: username +## +## Value: String +## zone.external.mountpoint = devicebound/ + +## Whether use username replace client id +## +## Value: boolean +## Default: false +zone.external.use_username_as_clientid = false + +## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) +## +## Value: true | false +zone.external.ignore_loop_deliver = false + +## Whether to parse the MQTT frame in strict mode +## +## Value: true | false +zone.external.strict_mode = false + +## Specify the response information returned to the client +## +## Value: String +## zone.external.response_information = example + +##-------------------------------------------------------------------- +## Internal Zone + +zone.internal.allow_anonymous = true + +## Enable per connection stats. +## +## Value: Flag +zone.internal.enable_stats = on + +## Enable ACL check. +## +## Value: Flag +zone.internal.enable_acl = off + +## The action when acl check reject current operation +## +## Value: ignore | disconnect +## Default: ignore +zone.internal.acl_deny_action = ignore + +## See zone.$name.force_gc_policy +## zone.internal.force_gc_policy = 128000|128MB + +## See zone.$name.wildcard_subscription. +## +## Value: boolean +## zone.internal.wildcard_subscription = true + +## See zone.$name.shared_subscription. +## +## Value: boolean +## zone.internal.shared_subscription = true + +## See zone.$name.max_subscriptions. +## +## Value: Integer +zone.internal.max_subscriptions = 0 + +## See zone.$name.max_inflight +## +## Value: Number +zone.internal.max_inflight = 128 + +## See zone.$name.max_awaiting_rel +## +## Value: Number +zone.internal.max_awaiting_rel = 1000 + +## See zone.$name.max_mqueue_len +## +## Value: Number >= 0 +zone.internal.max_mqueue_len = 10000 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.internal.mqueue_store_qos0 = true + +## Whether to turn on flapping detect +## +## Value: on | off +zone.internal.enable_flapping_detect = off + +## See zone.$name.force_shutdown_policy +## +## Default: +## - 10000|64MB on ARCH_64 system +## - 1000|32MB on ARCH_32 sytem +#zone.internal.force_shutdown_policy = 10000|64MB + +## All the topics will be prefixed with the mountpoint path if this option is enabled. +## +## Variables in mountpoint path: +## - %c: clientid +## - %u: username +## +## Value: String +## zone.internal.mountpoint = cloudbound/ + +## Whether to ignore loop delivery of messages.(for mqtt v3.1.1) +## +## Value: true | false +zone.internal.ignore_loop_deliver = false + +## Whether to parse the MQTT frame in strict mode +## +## Value: true | false +zone.internal.strict_mode = false + +## Specify the response information returned to the client +## +## Value: String +## zone.internal.response_information = example + +## Allow the zone's clients to bypass authentication step +## +## Value: true | false +zone.internal.bypass_auth_plugins = true + +## CONFIG_SECTION_END=zones ==================================================== + +## CONFIG_SECTION_BGN=listeners ================================================ + +##-------------------------------------------------------------------- +## MQTT/TCP - External TCP Listener for MQTT Protocol + +## listener.tcp.$name is the IP address and port that the MQTT/TCP +## listener will bind. +## +## Value: IP:Port | Port +## +## Examples: 1883, 127.0.0.1:1883, ::1:1883 +listener.tcp.external = 0.0.0.0:1883 + +## The acceptor pool for external MQTT/TCP listener. +## +## Value: Number +listener.tcp.external.acceptors = 8 + +## Maximum number of concurrent MQTT/TCP connections. +## +## Value: Number +listener.tcp.external.max_connections = 1024000 + +## Maximum external connections per second. +## +## Value: Number +listener.tcp.external.max_conn_rate = 1000 + +## Specify the {active, N} option for the external MQTT/TCP Socket. +## +## Value: Number +listener.tcp.external.active_n = 100 + +## Zone of the external MQTT/TCP listener belonged to. +## +## See: zone.$name.* +## +## Value: String +listener.tcp.external.zone = external + +## The access control rules for the MQTT/TCP listener. +## +## See: https://github.com/emqtt/esockd#allowdeny +## +## Value: ACL Rule +## +## Example: allow 192.168.0.0/24 +listener.tcp.external.access.1 = allow all + +## Enable the Proxy Protocol V1/2 if the EMQX cluster is deployed +## behind HAProxy or Nginx. +## +## See: https://www.haproxy.com/blog/haproxy/proxy-protocol/ +## +## Value: on | off +## listener.tcp.external.proxy_protocol = on + +## Sets the timeout for proxy protocol. EMQX will close the TCP connection +## if no proxy protocol packet recevied within the timeout. +## +## Value: Duration +## listener.tcp.external.proxy_protocol_timeout = 3s + +## Enable the option for X.509 certificate based authentication. +## EMQX will use the common name of certificate as MQTT username. +## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info +## +## Value: cn +## listener.tcp.external.peer_cert_as_username = cn + +## Enable the option for X.509 certificate based authentication. +## EMQX will use the common name of certificate as MQTT clientid. +## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info +## +## Value: cn +## listener.tcp.external.peer_cert_as_clientid = cn + +## The TCP backlog defines the maximum length that the queue of pending +## connections can grow to. +## +## Value: Number >= 0 +listener.tcp.external.backlog = 1024 + +## The TCP send timeout for external MQTT connections. +## +## Value: Duration +listener.tcp.external.send_timeout = 15s + +## Close the TCP connection if send timeout. +## +## Value: on | off +listener.tcp.external.send_timeout_close = on + +## The TCP receive buffer(os kernel) for MQTT connections. +## +## See: http://erlang.org/doc/man/inet.html +## +## Value: Bytes +## listener.tcp.external.recbuf = 2KB + +## The TCP send buffer(os kernel) for MQTT connections. +## +## See: http://erlang.org/doc/man/inet.html +## +## Value: Bytes +## listener.tcp.external.sndbuf = 2KB + +## The size of the user-level software buffer used by the driver. +## Not to be confused with options sndbuf and recbuf, which correspond +## to the Kernel socket buffers. It is recommended to have val(buffer) +## >= max(val(sndbuf),val(recbuf)) to avoid performance issues because +## of unnecessary copying. val(buffer) is automatically set to the above +## maximum when values sndbuf or recbuf are set. +## +## See: http://erlang.org/doc/man/inet.html +## +## Value: Bytes +## listener.tcp.external.buffer = 2KB + +## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. +## +## Value: on | off +## listener.tcp.external.tune_buffer = off + +## The socket is set to a busy state when the amount of data queued internally +## by the ERTS socket implementation reaches this limit. +## +## Value: on | off +## Defaults to 1MB +## listener.tcp.external.high_watermark = 1MB + +## The TCP_NODELAY flag for MQTT connections. Small amounts of data are +## sent immediately if the option is enabled. +## +## Value: true | false +listener.tcp.external.nodelay = true + +## The SO_REUSEADDR flag for TCP listener. +## +## Value: true | false +listener.tcp.external.reuseaddr = true + +##-------------------------------------------------------------------- +## Internal TCP Listener for MQTT Protocol + +## The IP address and port that the internal MQTT/TCP protocol listener +## will bind. +## +## Value: IP:Port, Port +## +## Examples: 11883, 127.0.0.1:11883, ::1:11883 +listener.tcp.internal = 127.0.0.1:11883 + +## The acceptor pool for internal MQTT/TCP listener. +## +## Value: Number +listener.tcp.internal.acceptors = 4 + +## Maximum number of concurrent MQTT/TCP connections. +## +## Value: Number +listener.tcp.internal.max_connections = 1024000 + +## Maximum internal connections per second. +## +## Value: Number +listener.tcp.internal.max_conn_rate = 1000 + +## Specify the {active, N} option for the internal MQTT/TCP Socket. +## +## Value: Number +listener.tcp.internal.active_n = 1000 + +## Zone of the internal MQTT/TCP listener belonged to. +## +## Value: String +listener.tcp.internal.zone = internal + +## The TCP backlog of internal MQTT/TCP Listener. +## +## See: listener.tcp.$name.backlog +## +## Value: Number >= 0 +listener.tcp.internal.backlog = 512 + +## The TCP send timeout for internal MQTT connections. +## +## See: listener.tcp.$name.send_timeout +## +## Value: Duration +listener.tcp.internal.send_timeout = 5s + +## Close the MQTT/TCP connection if send timeout. +## +## See: listener.tcp.$name.send_timeout_close +## +## Value: on | off +listener.tcp.internal.send_timeout_close = on + +## The TCP receive buffer(os kernel) for internal MQTT connections. +## +## See: listener.tcp.$name.recbuf +## +## Value: Bytes +listener.tcp.internal.recbuf = 64KB + +## The TCP send buffer(os kernel) for internal MQTT connections. +## +## See: http://erlang.org/doc/man/inet.html +## +## Value: Bytes +listener.tcp.internal.sndbuf = 64KB + +## The size of the user-level software buffer used by the driver. +## +## See: listener.tcp.$name.buffer +## +## Value: Bytes +## listener.tcp.internal.buffer = 16KB + +## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. +## +## See: listener.tcp.$name.tune_buffer +## +## Value: on | off +## listener.tcp.internal.tune_buffer = off + +## The TCP_NODELAY flag for internal MQTT connections. +## +## See: listener.tcp.$name.nodelay +## +## Value: true | false +listener.tcp.internal.nodelay = false + +## The SO_REUSEADDR flag for MQTT/TCP Listener. +## +## Value: true | false +listener.tcp.internal.reuseaddr = true + +##-------------------------------------------------------------------- +## MQTT/SSL - External SSL Listener for MQTT Protocol + +## listener.ssl.$name is the IP address and port that the MQTT/SSL +## listener will bind. +## +## Value: IP:Port | Port +## +## Examples: 8883, 127.0.0.1:8883, ::1:8883 +listener.ssl.external = 8883 + +## The acceptor pool for external MQTT/SSL listener. +## +## Value: Number +listener.ssl.external.acceptors = 16 + +## Maximum number of concurrent MQTT/SSL connections. +## +## Value: Number +listener.ssl.external.max_connections = 102400 + +## Maximum MQTT/SSL connections per second. +## +## Value: Number +listener.ssl.external.max_conn_rate = 500 + +## Specify the {active, N} option for the internal MQTT/SSL Socket. +## +## Value: Number +listener.ssl.external.active_n = 100 + +## Zone of the external MQTT/SSL listener belonged to. +## +## Value: String +listener.ssl.external.zone = external + +## The access control rules for the MQTT/SSL listener. +## +## See: listener.tcp.$name.access +## +## Value: ACL Rule +listener.ssl.external.access.1 = allow all + +## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind +## HAProxy or Nginx. +## +## See: listener.tcp.$name.proxy_protocol +## +## Value: on | off +## listener.ssl.external.proxy_protocol = on + +## Sets the timeout for proxy protocol. +## +## See: listener.tcp.$name.proxy_protocol_timeout +## +## Value: Duration +## listener.ssl.external.proxy_protocol_timeout = 3s + +## TLS versions only to protect from POODLE attack. +## +## See: http://erlang.org/doc/man/ssl.html +## +## Value: String, seperated by ',' +## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier +## listener.ssl.external.tls_versions = tlsv1.3,tlsv1.2,tlsv1.1,tlsv1 + +## TLS Handshake timeout. +## +## Value: Duration +listener.ssl.external.handshake_timeout = 15s + +## Maximum number of non-self-issued intermediate certificates that +## can follow the peer certificate in a valid certification path. +## +## Value: Number +## listener.ssl.external.depth = 10 + +## String containing the user's password. Only used if the private keyfile +## is password-protected. +## +## Value: String +## listener.ssl.external.key_password = yourpass + +## Path to the file containing the user's private PEM-encoded key. +## +## See: http://erlang.org/doc/man/ssl.html +## +## Value: File +listener.ssl.external.keyfile = etc/certs/key.pem + +## Path to a file containing the user certificate. +## +## See: http://erlang.org/doc/man/ssl.html +## +## Value: File +listener.ssl.external.certfile = etc/certs/cert.pem + +## Path to the file containing PEM-encoded CA certificates. The CA certificates +## are used during server authentication and when building the client certificate chain. +## +## Value: File +## listener.ssl.external.cacertfile = etc/certs/cacert.pem + +## The Ephemeral Diffie-Helman key exchange is a very effective way of +## ensuring Forward Secrecy by exchanging a set of keys that never hit +## the wire. Since the DH key is effectively signed by the private key, +## it needs to be at least as strong as the private key. In addition, +## the default DH groups that most of the OpenSSL installations have +## are only a handful (since they are distributed with the OpenSSL +## package that has been built for the operating system it’s running on) +## and hence predictable (not to mention, 1024 bits only). +## In order to escape this situation, first we need to generate a fresh, +## strong DH group, store it in a file and then use the option above, +## to force our SSL application to use the new DH group. Fortunately, +## OpenSSL provides us with a tool to do that. Simply run: +## openssl dhparam -out dh-params.pem 2048 +## +## Value: File +## listener.ssl.external.dhfile = etc/certs/dh-params.pem + +## A server only does x509-path validation in mode verify_peer, +## as it then sends a certificate request to the client (this +## message is not sent if the verify option is verify_none). +## You can then also want to specify option fail_if_no_peer_cert. +## More information at: http://erlang.org/doc/man/ssl.html +## +## Value: verify_peer | verify_none +## listener.ssl.external.verify = verify_peer + +## Used together with {verify, verify_peer} by an SSL server. If set to true, +## the server fails if the client does not have a certificate to send, that is, +## sends an empty certificate. +## +## Value: true | false +## listener.ssl.external.fail_if_no_peer_cert = true + +## This is the single most important configuration option of an Erlang SSL +## application. Ciphers (and their ordering) define the way the client and +## server encrypt information over the wire, from the initial Diffie-Helman +## key exchange, the session key encryption ## algorithm and the message +## digest algorithm. Selecting a good cipher suite is critical for the +## application’s data security, confidentiality and performance. +## +## The cipher list above offers: +## +## A good balance between compatibility with older browsers. +## It can get stricter for Machine-To-Machine scenarios. +## Perfect Forward Secrecy. +## No old/insecure encryption and HMAC algorithms +## +## Most of it was copied from Mozilla’s Server Side TLS article +## +## Value: Ciphers +listener.ssl.external.ciphers = TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA + + +## Ciphers for TLS PSK. +## Note that 'listener.ssl.external.ciphers' and 'listener.ssl.external.psk_ciphers' cannot +## be configured at the same time. +## See 'https://tools.ietf.org/html/rfc4279#section-2'. +#listener.ssl.external.psk_ciphers = RSA-PSK-AES256-GCM-SHA384,RSA-PSK-AES256-CBC-SHA384,RSA-PSK-AES128-GCM-SHA256,RSA-PSK-AES128-CBC-SHA256,RSA-PSK-AES256-CBC-SHA,RSA-PSK-AES128-CBC-SHA + +## SSL parameter renegotiation is a feature that allows a client and a server +## to renegotiate the parameters of the SSL connection on the fly. +## RFC 5746 defines a more secure way of doing this. By enabling secure renegotiation, +## you drop support for the insecure renegotiation, prone to MitM attacks. +## +## Value: on | off +## listener.ssl.external.secure_renegotiate = off + +## A performance optimization setting, it allows clients to reuse +## pre-existing sessions, instead of initializing new ones. +## Read more about it here. +## +## See: http://erlang.org/doc/man/ssl.html +## +## Value: on | off +## listener.ssl.external.reuse_sessions = on + +## An important security setting, it forces the cipher to be set based +## on the server-specified order instead of the client-specified order, +## hence enforcing the (usually more properly configured) security +## ordering of the server administrator. +## +## Value: on | off +## listener.ssl.external.honor_cipher_order = on + +## Use the CN, DN or CRT field from the client certificate as a username. +## Notice that 'verify' should be set as 'verify_peer'. +## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. +## +## Value: cn | dn | crt | pem | md5 +## listener.ssl.external.peer_cert_as_username = cn + +## Use the CN, DN or CRT field from the client certificate as a username. +## Notice that 'verify' should be set as 'verify_peer'. +## 'pem' encodes CRT in base64, and md5 is the md5 hash of CRT. +## +## Value: cn | dn | crt | pem | md5 +## listener.ssl.external.peer_cert_as_clientid = cn + +## TCP backlog for the SSL connection. +## +## See listener.tcp.$name.backlog +## +## Value: Number >= 0 +## listener.ssl.external.backlog = 1024 + +## The TCP send timeout for the SSL connection. +## +## See listener.tcp.$name.send_timeout +## +## Value: Duration +## listener.ssl.external.send_timeout = 15s + +## Close the SSL connection if send timeout. +## +## See: listener.tcp.$name.send_timeout_close +## +## Value: on | off +## listener.ssl.external.send_timeout_close = on + +## The TCP receive buffer(os kernel) for the SSL connections. +## +## See: listener.tcp.$name.recbuf +## +## Value: Bytes +## listener.ssl.external.recbuf = 4KB + +## The TCP send buffer(os kernel) for internal MQTT connections. +## +## See: listener.tcp.$name.sndbuf +## +## Value: Bytes +## listener.ssl.external.sndbuf = 4KB + +## The size of the user-level software buffer used by the driver. +## +## See: listener.tcp.$name.buffer +## +## Value: Bytes +## listener.ssl.external.buffer = 4KB + +## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. +## +## See: listener.tcp.$name.tune_buffer +## +## Value: on | off +## listener.ssl.external.tune_buffer = off + +## The TCP_NODELAY flag for SSL connections. +## +## See: listener.tcp.$name.nodelay +## +## Value: true | false +## listener.ssl.external.nodelay = true + +## The SO_REUSEADDR flag for MQTT/SSL Listener. +## +## Value: true | false +listener.ssl.external.reuseaddr = true + +##-------------------------------------------------------------------- +## External WebSocket listener for MQTT protocol + +## listener.ws.$name is the IP address and port that the MQTT/WebSocket +## listener will bind. +## +## Value: IP:Port | Port +## +## Examples: 8083, 127.0.0.1:8083, ::1:8083 +listener.ws.external = 8083 + +## The path of WebSocket MQTT endpoint +## +## Value: URL Path +listener.ws.external.mqtt_path = /mqtt + +## The acceptor pool for external MQTT/WebSocket listener. +## +## Value: Number +listener.ws.external.acceptors = 4 + +## Maximum number of concurrent MQTT/WebSocket connections. +## +## Value: Number +listener.ws.external.max_connections = 102400 + +## Maximum MQTT/WebSocket connections per second. +## +## Value: Number +listener.ws.external.max_conn_rate = 1000 + +## Simulate the {active, N} option for the MQTT/WebSocket connections. +## +## Value: Number +listener.ws.external.active_n = 100 + +## Zone of the external MQTT/WebSocket listener belonged to. +## +## Value: String +listener.ws.external.zone = external + +## The access control for the MQTT/WebSocket listener. +## +## See: listener.ws.$name.access +## +## Value: ACL Rule +listener.ws.external.access.1 = allow all + +## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. +## Set to false for WeChat MiniApp. +## +## Value: true | false +## listener.ws.external.fail_if_no_subprotocol = true + +## Supported subprotocols +## +## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 +## listener.ws.external.supported_subprotocols = mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 + +## Specify which HTTP header for real source IP if the EMQX cluster is +## deployed behind NGINX or HAProxy. +## +## Default: X-Forwarded-For +## listener.ws.external.proxy_address_header = X-Forwarded-For + +## Specify which HTTP header for real source port if the EMQX cluster is +## deployed behind NGINX or HAProxy. +## +## Default: X-Forwarded-Port +## listener.ws.external.proxy_port_header = X-Forwarded-Port + +## Enable the Proxy Protocol V1/2 if the EMQ cluster is deployed behind +## HAProxy or Nginx. +## +## See: listener.ws.$name.proxy_protocol +## +## Value: on | off +## listener.ws.external.proxy_protocol = on + +## Sets the timeout for proxy protocol. +## +## See: listener.ws.$name.proxy_protocol_timeout +## +## Value: Duration +## listener.ws.external.proxy_protocol_timeout = 3s + +## Enable the option for X.509 certificate based authentication. +## EMQX will use the common name of certificate as MQTT username. +## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info +## +## Value: cn +## listener.ws.external.peer_cert_as_username = cn + +## Enable the option for X.509 certificate based authentication. +## EMQX will use the common name of certificate as MQTT clientid. +## Only support Proxy Protocol V2, the CN is available in Proxy Protocol V2 additional info +## +## Value: cn +## listener.ws.external.peer_cert_as_clientid = cn + +## The TCP backlog of external MQTT/WebSocket Listener. +## +## See: listener.ws.$name.backlog +## +## Value: Number >= 0 +listener.ws.external.backlog = 1024 + +## The TCP send timeout for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.send_timeout +## +## Value: Duration +listener.ws.external.send_timeout = 15s + +## Close the MQTT/WebSocket connection if send timeout. +## +## See: listener.ws.$name.send_timeout_close +## +## Value: on | off +listener.ws.external.send_timeout_close = on + +## The TCP receive buffer(os kernel) for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.recbuf +## +## Value: Bytes +## listener.ws.external.recbuf = 2KB + +## The TCP send buffer(os kernel) for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.sndbuf +## +## Value: Bytes +## listener.ws.external.sndbuf = 2KB + +## The size of the user-level software buffer used by the driver. +## +## See: listener.ws.$name.buffer +## +## Value: Bytes +## listener.ws.external.buffer = 2KB + +## Sets the 'buffer = max(sndbuf, recbuf)' if this option is enabled. +## +## See: listener.ws.$name.tune_buffer +## +## Value: on | off +## listener.ws.external.tune_buffer = off + +## The TCP_NODELAY flag for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.nodelay +## +## Value: true | false +listener.ws.external.nodelay = true + +## The compress flag for external MQTT/WebSocket connections. +## +## If this Value is set true,the websocket message would be compressed +## +## Value: true | false +## listener.ws.external.compress = true + +## The level of deflate options for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.level +## +## Value: none | default | best_compression | best_speed +## listener.ws.external.deflate_opts.level = default + +## The mem_level of deflate options for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.mem_level +## +## Valid range is 1-9 +## listener.ws.external.deflate_opts.mem_level = 8 + +## The strategy of deflate options for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.strategy +## +## Value: default | filtered | huffman_only | rle +## listener.ws.external.deflate_opts.strategy = default + +## The deflate option for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.server_context_takeover +## +## Value: takeover | no_takeover +## listener.ws.external.deflate_opts.server_context_takeover = takeover + +## The deflate option for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.client_context_takeover +## +## Value: takeover | no_takeover +## listener.ws.external.deflate_opts.client_context_takeover = takeover + +## The deflate options for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.server_max_window_bits +## +## Valid range is 8-15 +## listener.ws.external.deflate_opts.server_max_window_bits = 15 + +## The deflate options for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.deflate_opts.client_max_window_bits +## +## Valid range is 8-15 +## listener.ws.external.deflate_opts.client_max_window_bits = 15 + +## The idle timeout for external MQTT/WebSocket connections. +## +## See: listener.ws.$name.idle_timeout +## +## Value: Duration +## listener.ws.external.idle_timeout = 60s + +## The max frame size for external MQTT/WebSocket connections. +## +## +## Value: Number +## listener.ws.external.max_frame_size = 0 + +## Whether a WebSocket message is allowed to contain multiple MQTT packets +## +## Value: single | multiple +listener.ws.external.mqtt_piggyback = multiple + +## By default, EMQX web socket connection does not restrict connections to specific origins. +## It also, by default, does not enforce the presence of origin in request headers for WebSocket connections. +## Because of this, a malicious user could potentially hijack an existing web-socket connection to EMQX. + +## To prevent this, users can set allowed origin headers in their ws connection to EMQX. +## WS configs are set in listener.ws.external.* +## WSS configs are set in listener.wss.external.* + +## Example for WS connection +## To enables origin check in header for websocket connnection, +## set `listener.ws.external.check_origin_enable = true`. By default it is false, +## When it is set to true and no origin is present in the header of a ws connection request, the request fails. + +## To allow origins to be absent in header in the websocket connection when check_origin_enable is true, +## set `listener.ws.external.allow_origin_absence = true` + +## Enabling origin check implies there are specific valid origins allowed for ws connection. +## To set the list of allowed origins in header for websocket connection +## listener.ws.external.check_origins = http://localhost:18083(localhost dashboard url), http://yourapp.com` +## check_origins config allows a comma separated list of origins so you can specify as many origins are you want. +## With these configs, you can allow only connections from only authorized origins to your broker + +## Enable origin check in header for websocket connection +## +## Value: true | false (default false) +listener.ws.external.check_origin_enable = false + +## Allow origin to be absent in header in websocket connection when check_origin_enable is true +## +## Value: true | false (default true) +listener.ws.external.allow_origin_absence = true + +## Comma separated list of allowed origin in header for websocket connection +## +## Value: http://url eg. local http dashboard url - http://localhost:18083, http://127.0.0.1:18083 +listener.ws.external.check_origins = http://localhost:18083, http://127.0.0.1:18083 + +##-------------------------------------------------------------------- +## External WebSocket/SSL listener for MQTT Protocol + +## listener.wss.$name is the IP address and port that the MQTT/WebSocket/SSL +## listener will bind. +## +## Value: IP:Port | Port +## +## Examples: 8084, 127.0.0.1:8084, ::1:8084 +listener.wss.external = 8084 + +## The path of WebSocket MQTT endpoint +## +## Value: URL Path +listener.wss.external.mqtt_path = /mqtt + +## The acceptor pool for external MQTT/WebSocket/SSL listener. +## +## Value: Number +listener.wss.external.acceptors = 4 + +## Maximum number of concurrent MQTT/Webwocket/SSL connections. +## +## Value: Number +listener.wss.external.max_connections = 102400 + +## Maximum MQTT/WebSocket/SSL connections per second. +## +## See: listener.tcp.$name.max_conn_rate +## +## Value: Number +listener.wss.external.max_conn_rate = 1000 + +## Simulate the {active, N} option for the MQTT/WebSocket/SSL connections. +## +## Value: Number +listener.wss.external.active_n = 100 + +## Zone of the external MQTT/WebSocket/SSL listener belonged to. +## +## Value: String +listener.wss.external.zone = external + +## The access control rules for the MQTT/WebSocket/SSL listener. +## +## See: listener.tcp.$name.access. +## +## Value: ACL Rule +listener.wss.external.access.1 = allow all + +## If set to true, the server fails if the client does not have a Sec-WebSocket-Protocol to send. +## Set to false for WeChat MiniApp. +## +## Value: true | false +## listener.wss.external.fail_if_no_subprotocol = true + +## Supported subprotocols +## +## Default: mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 +## listener.wss.external.supported_subprotocols = mqtt, mqtt-v3, mqtt-v3.1.1, mqtt-v5 + +## Specify which HTTP header for real source IP if the EMQX cluster is +## deployed behind NGINX or HAProxy. +## +## Default: X-Forwarded-For +## listener.wss.external.proxy_address_header = X-Forwarded-For + +## Specify which HTTP header for real source port if the EMQX cluster is +## deployed behind NGINX or HAProxy. +## +## Default: X-Forwarded-Port +## listener.wss.external.proxy_port_header = X-Forwarded-Port + +## Enable the Proxy Protocol V1/2 support. +## +## See: listener.tcp.$name.proxy_protocol +## +## Value: on | off +## listener.wss.external.proxy_protocol = on + +## Sets the timeout for proxy protocol. +## +## See: listener.tcp.$name.proxy_protocol_timeout +## +## Value: Duration +## listener.wss.external.proxy_protocol_timeout = 3s + +## TLS versions only to protect from POODLE attack. +## +## See: listener.ssl.$name.tls_versions +## +## Value: String, seperated by ',' +## NOTE: Do not use tlsv1.3 if emqx is running on OTP-22 or earlier +## listener.wss.external.tls_versions = tlsv1.3,tlsv1.2,tlsv1.1,tlsv1 + +## Path to the file containing the user's private PEM-encoded key. +## +## See: listener.ssl.$name.keyfile +## +## Value: File +listener.wss.external.keyfile = etc/certs/key.pem + +## Path to a file containing the user certificate. +## +## See: listener.ssl.$name.certfile +## +## Value: File +listener.wss.external.certfile = etc/certs/cert.pem + +## Path to the file containing PEM-encoded CA certificates. +## +## See: listener.ssl.$name.cacert +## +## Value: File +## listener.wss.external.cacertfile = etc/certs/cacert.pem + +## Maximum number of non-self-issued intermediate certificates that +## can follow the peer certificate in a valid certification path. +## +## See: listener.ssl.external.depth +## +## Value: Number +## listener.wss.external.depth = 10 + +## String containing the user's password. Only used if the private keyfile +## is password-protected. +## +## See: listener.ssl.$name.key_password +## +## Value: String +## listener.wss.external.key_password = yourpass + +## See: listener.ssl.$name.dhfile +## +## Value: File +## listener.ssl.external.dhfile = etc/certs/dh-params.pem + +## See: listener.ssl.$name.verify +## +## Value: verify_peer | verify_none +## listener.wss.external.verify = verify_peer + +## See: listener.ssl.$name.fail_if_no_peer_cert +## +## Value: false | true +## listener.wss.external.fail_if_no_peer_cert = true + +## See: listener.ssl.$name.ciphers +## +## Value: Ciphers +listener.wss.external.ciphers = TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA + +## Ciphers for TLS PSK. +## Note that 'listener.wss.external.ciphers' and 'listener.wss.external.psk_ciphers' cannot +## be configured at the same time. +## See 'https://tools.ietf.org/html/rfc4279#section-2'. +## listener.wss.external.psk_ciphers = RSA-PSK-AES256-GCM-SHA384,RSA-PSK-AES256-CBC-SHA384,RSA-PSK-AES128-GCM-SHA256,RSA-PSK-AES128-CBC-SHA256,RSA-PSK-AES256-CBC-SHA,RSA-PSK-AES128-CBC-SHA + +## See: listener.ssl.$name.secure_renegotiate +## +## Value: on | off +## listener.wss.external.secure_renegotiate = off + +## See: listener.ssl.$name.reuse_sessions +## +## Value: on | off +## listener.wss.external.reuse_sessions = on + +## See: listener.ssl.$name.honor_cipher_order +## +## Value: on | off +## listener.wss.external.honor_cipher_order = on + +## See: listener.ssl.$name.peer_cert_as_username +## +## Value: cn | dn | crt | pem | md5 +## listener.wss.external.peer_cert_as_username = cn + +## See: listener.ssl.$name.peer_cert_as_clientid +## +## Value: cn | dn | crt | pem | md5 +## listener.wss.external.peer_cert_as_clientid = cn + +## TCP backlog for the WebSocket/SSL connection. +## +## See: listener.tcp.$name.backlog +## +## Value: Number >= 0 +listener.wss.external.backlog = 1024 + +## The TCP send timeout for the WebSocket/SSL connection. +## +## See: listener.tcp.$name.send_timeout +## +## Value: Duration +listener.wss.external.send_timeout = 15s + +## Close the WebSocket/SSL connection if send timeout. +## +## See: listener.tcp.$name.send_timeout_close +## +## Value: on | off +listener.wss.external.send_timeout_close = on + +## The TCP receive buffer(os kernel) for the WebSocket/SSL connections. +## +## See: listener.tcp.$name.recbuf +## +## Value: Bytes +## listener.wss.external.recbuf = 4KB + +## The TCP send buffer(os kernel) for the WebSocket/SSL connections. +## +## See: listener.tcp.$name.sndbuf +## +## Value: Bytes +## listener.wss.external.sndbuf = 4KB + +## The size of the user-level software buffer used by the driver. +## +## See: listener.tcp.$name.buffer +## +## Value: Bytes +## listener.wss.external.buffer = 4KB + +## The TCP_NODELAY flag for WebSocket/SSL connections. +## +## See: listener.tcp.$name.nodelay +## +## Value: true | false +## listener.wss.external.nodelay = true + +## The compress flag for external WebSocket/SSL connections. +## +## If this Value is set true,the websocket message would be compressed +## +## Value: true | false +## listener.wss.external.compress = true + +## The level of deflate options for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.level +## +## Value: none | default | best_compression | best_speed +## listener.wss.external.deflate_opts.level = default + +## The mem_level of deflate options for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.mem_level +## +## Valid range is 1-9 +## listener.wss.external.deflate_opts.mem_level = 8 + +## The strategy of deflate options for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.strategy +## +## Value: default | filtered | huffman_only | rle +## listener.wss.external.deflate_opts.strategy = default + +## The deflate option for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.server_context_takeover +## +## Value: takeover | no_takeover +## listener.wss.external.deflate_opts.server_context_takeover = takeover + +## The deflate option for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.client_context_takeover +## +## Value: takeover | no_takeover +## listener.wss.external.deflate_opts.client_context_takeover = takeover + +## The deflate options for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.server_max_window_bits +## +## Valid range is 8-15 +## listener.wss.external.deflate_opts.server_max_window_bits = 15 + +## The deflate options for external WebSocket/SSL connections. +## +## See: listener.wss.$name.deflate_opts.client_max_window_bits +## +## Valid range is 8-15 +## listener.wss.external.deflate_opts.client_max_window_bits = 15 + +## The idle timeout for external WebSocket/SSL connections. +## +## See: listener.wss.$name.idle_timeout +## +## Value: Duration +## listener.wss.external.idle_timeout = 60s + +## The max frame size for external WebSocket/SSL connections. +## +## Value: Number +## listener.wss.external.max_frame_size = 0 + +## Whether a WebSocket message is allowed to contain multiple MQTT packets +## +## Value: single | multiple +listener.wss.external.mqtt_piggyback = multiple +## Enable origin check in header for secure websocket connection +## +## Value: true | false (default false) +listener.wss.external.check_origin_enable = false +## Allow origin to be absent in header in secure websocket connection when check_origin_enable is true +## +## Value: true | false (default true) +listener.wss.external.allow_origin_absence = true +## Comma separated list of allowed origin in header for secure websocket connection +## +## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 +listener.wss.external.check_origins = https://localhost:8084, https://127.0.0.1:8084 + +## CONFIG_SECTION_END=listeners ================================================ + +## CONFIG_SECTION_BGN=modules ================================================== + +## The file to store loaded module names. +## +## Value: File +modules.loaded_file = data/loaded_modules + +##-------------------------------------------------------------------- +## Presence Module + +## Sets the QoS for presence MQTT message. +## +## Value: 0 | 1 | 2 +module.presence.qos = 1 + +##-------------------------------------------------------------------- +## Subscription Module + +## Subscribe the Topics automatically when client connected. +## +## Value: String +## module.subscription.1.topic = connected/%c/%u + +## Qos of the proxy subscription. +## +## Value: 0 | 1 | 2 +## Default: 0 +## module.subscription.1.qos = 0 + +## No Local of the proxy subscription options. +## This configuration only takes effect in the MQTT V5 protocol. +## +## Value: 0 | 1 +## Default: 0 +## module.subscription.1.nl = 0 + +## Retain As Published of the proxy subscription options. +## This configuration only takes effect in the MQTT V5 protocol. +## +## Value: 0 | 1 +## Default: 0 +## module.subscription.1.rap = 0 + +## Retain Handling of the proxy subscription options. +## This configuration only takes effect in the MQTT V5 protocol. +## +## Value: 0 | 1 | 2 +## Default: 0 +## module.subscription.1.rh = 0 + +##-------------------------------------------------------------------- +## Rewrite Module + +## {rewrite, Topic, Re, Dest} +## module.rewrite.pub.rule.1 = x/# ^x/y/(.+)$ z/y/$1 +## module.rewrite.sub.rule.1 = y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2 + +##-------------------------------------------------------------------- +## Slow Subscribers Statistics Module + +## the expire time of the record which in topk +## +## Value: 5 minutes +#module.slow_subs.expire_interval = 5m + +## maximum number of Top-K record +## +## Defalut: 10 +#module.slow_subs.top_k_num = 10 + +## Stats Type +## +## Default: whole +#module.slow_subs.stats_type = whole + +## Stats Threshold +## +## Default: 500ms +#module.slow_subs.threshold = 500ms + +## CONFIG_SECTION_END=modules ================================================== + +##------------------------------------------------------------------- +## Plugins +##------------------------------------------------------------------- + +## The etc dir for plugins' config. +## +## Value: Folder +plugins.etc_dir = etc/plugins/ + +## The file to store loaded plugin names. +## +## Value: File +plugins.loaded_file = data/loaded_plugins + +## The directory of extension plugins. +## +## Value: File +plugins.expand_plugins_dir = etc/plugins/ + +##-------------------------------------------------------------------- +## Broker +##-------------------------------------------------------------------- + +## System interval of publishing $SYS messages. +## +## Value: Duration +## Default: 1m, 1 minute +broker.sys_interval = 1m + +## System heartbeat interval of publishing following heart beat message: +## - "$SYS/brokers//uptime" +## - "$SYS/brokers//datetime" +## +## Value: Duration +## Default: 30s +broker.sys_heartbeat = 30s + +## Session locking strategy in a cluster. +## +## Value: Enum +## - local +## - leader +## - quorum +## - all +broker.session_locking_strategy = quorum + +## Dispatch strategy for shared subscription +## +## Value: Enum +## - hash_clientid +## - hash # same as hash_clientid +## - hash_topic +## - local +## - random +## - round_robin +## - sticky +broker.shared_subscription_strategy = random + +## Per group dispatch strategy for shared subscription +## +## Value: Enum +## - hash_clientid +## - hash # same as hash_clientid +## - hash_topic +## - local +## - random +## - round_robin +## - sticky +#broker.sample_group.shared_subscription_strategy = local + +## Enable/disable shared dispatch acknowledgement for QoS1 and QoS2 messages +## This should allow messages to be dispatched to a different subscriber in +## the group in case the picked (based on shared_subscription_strategy) one # is offline +## +## Value: Enum +## - true +## - false +broker.shared_dispatch_ack_enabled = false + +## Enable batch clean for deleted routes. +## +## Value: Flag +broker.route_batch_clean = off + +## Performance toggle for subscribe/unsubscribe wildcard topic. +## Change this toggle only when there are many wildcard topics. +## Value: Enum +## - key: mnesia translational updates with per-key locks. recommended for single node setup. +## - tab: mnesia translational updates with table lock. recommended for multi-nodes setup. +## - global: global lock protected updates. recommended for larger cluster. +## NOTE: when changing from/to 'global' lock, it requires all nodes in the cluster +## to be stopped before the change. +# broker.perf.route_lock_type = key + +## Enable trie path compaction. +## Enabling it significantly improves wildcard topic subscribe rate, +## if wildcard topics have unique prefixes like: 'sensor//+/', +## where ID is unique per subscriber. +## +## Topic match performance (when publishing) may degrade if messages +## are mostly published to topics with large number of levels. +## +## NOTE: This is a cluster-wide configuration. +## It rquires all nodes to be stopped before changing it. +## +## Value: Enum +## - true: enable trie path compaction +## - false: disable trie path compaction +# broker.perf.trie_compaction = true + +## CONFIG_SECTION_BGN=sys_mon ================================================== + +## Enable Long GC monitoring. Disable if the value is 0. +## Notice: don't enable the monitor in production for: +## https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 +## +## Value: Duration +## - h: hour +## - m: minute +## - s: second +## - ms: milliseconds +## +## Examples: +## - 2h: 2 hours +## - 30m: 30 minutes +## - 0.1s: 0.1 seconds +## - 100ms : 100 milliseconds +## +## Default: 0ms +sysmon.long_gc = 0 + +## Enable Long Schedule(ms) monitoring. +## +## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 +## +## Value: Duration +## - h: hour +## - m: minute +## - s: second +## - ms: milliseconds +## +## Examples: +## - 2h: 2 hours +## - 30m: 30 minutes +## - 0.1s: 0.1 seconds +## - 100ms: 100 milliseconds +## +## Default: 0ms +sysmon.long_schedule = 240ms + +## Enable Large Heap monitoring. +## +## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 +## +## Value: bytes +## +## Default: 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. +sysmon.large_heap = 8MB + +## Enable Busy Port monitoring. +## +## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 +## +## Value: true | false +sysmon.busy_port = false + +## Enable Busy Dist Port monitoring. +## +## See: http://erlang.org/doc/man/erlang.html#system_monitor-2 +## +## Value: true | false +sysmon.busy_dist_port = true + +## The time interval for the periodic cpu check +## +## Value: Duration +## -h: hour, e.g. '2h' for 2 hours +## -m: minute, e.g. '5m' for 5 minutes +## -s: second, e.g. '30s' for 30 seconds +## +## Default: 60s +os_mon.cpu_check_interval = 60s + +## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is set. +## +## Default: 80% +os_mon.cpu_high_watermark = 80% + +## The threshold, as percentage of system cpu, for how much system cpu can be used before the corresponding alarm is clear. +## +## Default: 60% +os_mon.cpu_low_watermark = 60% + +## The time interval for the periodic memory check +## +## Value: Duration +## -h: hour, e.g. '2h' for 2 hours +## -m: minute, e.g. '5m' for 5 minutes +## -s: second, e.g. '30s' for 30 seconds +## +## Default: 60s +os_mon.mem_check_interval = 60s + +## The threshold, as percentage of system memory, for how much system memory can be allocated before the corresponding alarm is set. +## +## Default: 70% +os_mon.sysmem_high_watermark = 70% + +## The threshold, as percentage of system memory, for how much system memory can be allocated by one Erlang process before the corresponding alarm is set. +## +## Default: 5% +os_mon.procmem_high_watermark = 5% + +## The time interval for the periodic process limit check +## +## Value: Duration +## +## Default: 30s +vm_mon.check_interval = 30s + +## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is set. +## +## Default: 80% +vm_mon.process_high_watermark = 80% + +## The threshold, as percentage of processes, for how many processes can simultaneously exist at the local node before the corresponding alarm is clear. +## +## Default: 60% +vm_mon.process_low_watermark = 60% + +## Specifies the actions to take when an alarm is activated +## +## Value: String +## - log +## - publish +## +## Default: log,publish +alarm.actions = log,publish + +## The maximum number of deactivated alarms +## +## Value: Integer +## +## Default: 1000 +alarm.size_limit = 1000 + +## Validity Period of deactivated alarms +## +## Value: Duration +## - h: hour +## - m: minute +## - s: second +## - ms: milliseconds +## +## Default: 24h +alarm.validity_period = 24h + +## CONFIG_SECTION_END=sys_mon ================================================== diff --git a/vendor/php-mqtt/client/.ci/hivemq.xml b/vendor/php-mqtt/client/.ci/hivemq.xml new file mode 100644 index 0000000..a0f7fab --- /dev/null +++ b/vendor/php-mqtt/client/.ci/hivemq.xml @@ -0,0 +1,56 @@ + + + + + + 1883 + 0.0.0.0 + + + + + 8883 + 0.0.0.0 + + + /hivemq-certs/server.jks + s3cr3t + s3cr3t + + + TLSv1.3 + TLSv1.2 + TLSv1.1 + TLSv1 + + + + + + + 8884 + 0.0.0.0 + + REQUIRED + + /hivemq-certs/ca.jks + s3cr3t + + + /hivemq-certs/server.jks + s3cr3t + s3cr3t + + + TLSv1.3 + TLSv1.2 + TLSv1.1 + TLSv1 + + + + + + diff --git a/vendor/php-mqtt/client/.ci/mosquitto.conf b/vendor/php-mqtt/client/.ci/mosquitto.conf new file mode 100644 index 0000000..c73fe4e --- /dev/null +++ b/vendor/php-mqtt/client/.ci/mosquitto.conf @@ -0,0 +1,31 @@ +# Config file for mosquitto +per_listener_settings true + +# Port to use for the default listener. +listener 1883 +allow_anonymous true + +# Port to use for the default listener with authentication. +listener 1884 +password_file /mosquitto/config/mosquitto.passwd +allow_anonymous false + +# ================================================================= +# Extra listeners +# ================================================================= + +# TLS listener without client certificate requirement +listener 8883 +cafile /mosquitto-certs/ca.crt +certfile /mosquitto-certs/server.crt +keyfile /mosquitto-certs/server.key +require_certificate false +allow_anonymous true + +# TLS listener with client certificate requirement +listener 8884 +cafile /mosquitto-certs/ca.crt +certfile /mosquitto-certs/server.crt +keyfile /mosquitto-certs/server.key +require_certificate true +allow_anonymous true diff --git a/vendor/php-mqtt/client/.ci/mosquitto.passwd b/vendor/php-mqtt/client/.ci/mosquitto.passwd new file mode 100644 index 0000000..3fec4c1 --- /dev/null +++ b/vendor/php-mqtt/client/.ci/mosquitto.passwd @@ -0,0 +1 @@ +ci-test-user:$6$QypQBNSQKE5bg6Ec$nzACfxhQ9qiYFByPPM/6GP/9kOWwDzEftN0EJPkS6M0PWqL55jAbBxUO863oWwhJ2q/YaubfLbe3xwwhBuoStQ== diff --git a/vendor/php-mqtt/client/.ci/rabbitmq.conf b/vendor/php-mqtt/client/.ci/rabbitmq.conf new file mode 100644 index 0000000..383caf9 --- /dev/null +++ b/vendor/php-mqtt/client/.ci/rabbitmq.conf @@ -0,0 +1,11 @@ +listeners.tcp.default = 5672 +loopback_users.guest = false + +mqtt.listeners.tcp.default = 1883 +mqtt.listeners.ssl = none +mqtt.allow_anonymous = true +mqtt.default_user = guest +mqtt.default_pass = guest +mqtt.vhost = / +mqtt.exchange = amq.topic +mqtt.subscription_ttl = 1800000 diff --git a/vendor/php-mqtt/client/.ci/tls/.gitignore b/vendor/php-mqtt/client/.ci/tls/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/vendor/php-mqtt/client/.ci/tls/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/vendor/php-mqtt/client/.github/dependabot.yml b/vendor/php-mqtt/client/.github/dependabot.yml new file mode 100644 index 0000000..b8e5835 --- /dev/null +++ b/vendor/php-mqtt/client/.github/dependabot.yml @@ -0,0 +1,23 @@ +version: 2 + +updates: + - package-ecosystem: "composer" + directory: "/" + allow: + - dependency-type: "development" + schedule: + interval: "daily" + time: "05:00" + timezone: "Europe/Vienna" + labels: + - "composer dependencies" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "05:00" + timezone: "Europe/Vienna" + labels: + - "github actions" diff --git a/vendor/php-mqtt/client/.github/release.yml b/vendor/php-mqtt/client/.github/release.yml new file mode 100644 index 0000000..374affd --- /dev/null +++ b/vendor/php-mqtt/client/.github/release.yml @@ -0,0 +1,25 @@ +changelog: + exclude: + labels: + - ignore-for-release + authors: + - octocat + categories: + - title: Added + labels: + - enhancement + - title: Deprecated + labels: + - deprecated + - title: Removed + labels: + - removed + - title: Fixed + labels: + - bug + - title: Security + labels: + - security + - title: Changed + labels: + - "*" diff --git a/vendor/php-mqtt/client/.github/workflows/dependency-review.yml b/vendor/php-mqtt/client/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..730f26b --- /dev/null +++ b/vendor/php-mqtt/client/.github/workflows/dependency-review.yml @@ -0,0 +1,23 @@ +name: 'Dependency Review' + +on: [pull_request] + +permissions: + contents: read + pull-requests: write + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + + - name: 'Dependency Review' + uses: actions/dependency-review-action@v3 + with: + comment-summary-in-pr: true + fail-on-scopes: 'runtime, development, unknown' + fail-on-severity: 'low' + license-check: true + vulnerability-check: true diff --git a/vendor/php-mqtt/client/.github/workflows/tests.yml b/vendor/php-mqtt/client/.github/workflows/tests.yml new file mode 100644 index 0000000..7aa9c09 --- /dev/null +++ b/vendor/php-mqtt/client/.github/workflows/tests.yml @@ -0,0 +1,140 @@ +name: Tests + +on: + push: + branches: + - master + pull_request_target: + types: [opened, synchronize, reopened] + +jobs: + test-all: + name: Test PHP ${{ matrix.php-version }} using broker [${{ matrix.mqtt-broker }}] + + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: ['8.0', '8.1', '8.2', '8.3'] + mqtt-broker: ['mosquitto-1.6', 'mosquitto-2.0', 'hivemq', 'emqx', 'rabbitmq'] + include: + - php-version: '8.3' + mqtt-broker: 'mosquitto-2.0' + run-sonarqube-analysis: true + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP ${{ matrix.php-version }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: phpunit:9.5.0 + coverage: pcov + + - name: Setup problem matchers for PHP + run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" + + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Composer dependencies + run: composer install --prefer-dist + + - name: Generate certificates for tests + run: | + sh create-certificates.sh + chmod u+rx,g+rx ${{ github.workspace }}/.ci/tls + chmod a+r ${{ github.workspace }}/.ci/tls/* + + - name: Start Mosquitto 1.6 message broker + if: matrix.mqtt-broker == 'mosquitto-1.6' + uses: Namoshek/mosquitto-github-action@v1 + with: + version: '1.6' + ports: '1883:1883 1884:1884 8883:8883 8884:8884' + certificates: ${{ github.workspace }}/.ci/tls + config: ${{ github.workspace }}/.ci/mosquitto.conf + password-file: ${{ github.workspace}}/.ci/mosquitto.passwd + + - name: Start Mosquitto 2.0 message broker + if: matrix.mqtt-broker == 'mosquitto-2.0' + uses: Namoshek/mosquitto-github-action@v1 + with: + version: '2.0' + ports: '1883:1883 1884:1884 8883:8883 8884:8884' + certificates: ${{ github.workspace }}/.ci/tls + config: ${{ github.workspace }}/.ci/mosquitto.conf + password-file: ${{ github.workspace}}/.ci/mosquitto.passwd + + - name: Start HiveMQ message broker + if: matrix.mqtt-broker == 'hivemq' + uses: Namoshek/hivemq4-github-action@v1 + with: + version: '4.8.5' + ports: '1883:1883 8883:8883 8884:8884' + certificates: ${{ github.workspace }}/.ci/tls + config: ${{ github.workspace }}/.ci/hivemq.xml + + - name: Start EMQ X message broker + if: matrix.mqtt-broker == 'emqx' + uses: Namoshek/emqx-github-action@v1.0.2 + with: + version: '4.4.3' + ports: '1883:1883' + config: ${{ github.workspace }}/.ci/emqx.conf + + - name: Start RabbitMQ message broker + if: matrix.mqtt-broker == 'rabbitmq' + uses: namoshek/rabbitmq-github-action@v1.1.0 + with: + version: '3.8.9' + ports: '1883:1883' + config: ${{ github.workspace }}/.ci/rabbitmq.conf + plugins: 'rabbitmq_mqtt' + + - name: Wait a bit until MQTT broker has started + run: sleep 45 + + - name: Run phpunit tests + run: composer test + env: + MQTT_BROKER_HOST: 'localhost' + MQTT_BROKER_PORT: 1883 + MQTT_BROKER_PORT_WITH_AUTHENTICATION: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && 1884 || 1883 }} + MQTT_BROKER_TLS_PORT: 8883 + MQTT_BROKER_TLS_WITH_CLIENT_CERT_PORT: 8884 + MQTT_BROKER_USERNAME: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && 'ci-test-user' || '' }} + MQTT_BROKER_PASSWORD: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && secrets.CI_MOSQUITTO_CI_TEST_USER_PASSWORD || '' }} + SKIP_TLS_TESTS: ${{ matrix.mqtt-broker == 'emqx' || matrix.mqtt-broker == 'rabbitmq' }} + + - name: Dump Docker logs on failure + if: failure() + uses: jwalton/gh-docker-logs@v2 + + - name: Prepare paths for SonarQube analysis + if: matrix.run-sonarqube-analysis + run: | + sed -i "s|$GITHUB_WORKSPACE|/github/workspace|g" phpunit.coverage-clover.xml + sed -i "s|$GITHUB_WORKSPACE|/github/workspace|g" phpunit.report-junit.xml + + - name: Run SonarQube analysis + uses: sonarsource/sonarcloud-github-action@v2.0.2 + if: matrix.run-sonarqube-analysis + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }} diff --git a/vendor/php-mqtt/client/.gitignore b/vendor/php-mqtt/client/.gitignore new file mode 100644 index 0000000..b111178 --- /dev/null +++ b/vendor/php-mqtt/client/.gitignore @@ -0,0 +1,6 @@ +.idea/ +.phpunit.result.cache +composer.lock +phpunit.coverage*.xml +phpunit.report*.xml +/vendor/ diff --git a/vendor/php-mqtt/client/.phpcs.xml b/vendor/php-mqtt/client/.phpcs.xml new file mode 100644 index 0000000..13a360d --- /dev/null +++ b/vendor/php-mqtt/client/.phpcs.xml @@ -0,0 +1,88 @@ + + + php-mqtt Code Style Standard + + + + + + + + + + + + + + + + + + + + + + src/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + src/* + + + + src + + + + + diff --git a/vendor/php-mqtt/client/CHANGELOG.md b/vendor/php-mqtt/client/CHANGELOG.md new file mode 100644 index 0000000..8da36e8 --- /dev/null +++ b/vendor/php-mqtt/client/CHANGELOG.md @@ -0,0 +1,72 @@ +# Changelog + +## Version `v1.0.0` + +Significant improvements to the architecture, API and design of the library have been part of `v1.0.0`. +Upgrading should be rather simple for most users though, since the public API has not changed a lot +and only in places which are not used too frequently. + +A lot of effort has been put into this summary to document as many changes as possible. +It is impossible to give a guarantee about the completeness of this list though. +You should cover your uses of the library with tests yourself as well. + +The following summary compares `v0.3.0` to `v1.0.0`. + +### Breaking Changes + +- The library does now require PHP 7.4 and supports PHP 8.0. This move was made with the clear intention to drop support for PHP 7.4 at some point. +- The primary interface and class of the library have been renamed to StudlyCaps to follow PSR-2: + - `\PhpMqtt\Client\Contracts\MQTTClient` → `\PhpMqtt\Client\Contracts\MqttClient` + - `\PhpMqtt\Client\MQTTClient` → `\PhpMqtt\Client\MqttClient` + - `\PhpMqtt\Client\Exceptions\MQTTClientException` → `\PhpMqtt\Client\Exceptions\MqttClientException` +- Protocol specific logic has been extracted from the `MQTTClient` class to a new interface `\PhpMqtt\Client\Contracts\MessageProcessor` and the respective implementation for MQTT 3.1, `\PhpMqtt\Client\MessageProcessors\Mqtt31MessageProcessor`. + - The `MessageProcessor` is responsible for parsing and building packages on a byte level. + - Splitting the logic from the main class did not only reduce the overall complexity of the class, it also made testing a lot easier and builds a solid foundation for future development and extension by implementing more protocol versions (like MQTT 5). +- Some `protected` properties have been changed to `private` to ensure they are not manipulated outside the offered scope, which is enforced through getters and setters. This change only affects users which actively inherited their own implementation from the library. +- The QoS 2 message flow is now implemented properly and should just work. + +#### Connection Settings + +- The `$caFile` parameter of the `MQTTClient` constructor as well as the `$username` and `$password` parameters of the `MQTTClient::connect()` method have been moved to the `ConnectionSettings` class. +- The `ConnectionSettings` use fluent setters for configuration now ([see README](README.md)). +- The `ConnectionSettings`` passed to `MQTTClient::connect()` are now validated and may not contain invalid configuration. In case of invalid configuration, a `\PhpMqtt\Client\Exceptions\ConfigurationInvalidException` is thrown. +- Additional TLS options have been added to the `ConnectionSettings` to support more uses cases with secured connections. + +#### Methods + +- Most methods can now throw a `\PhpMqtt\Client\Exceptions\RepositoryException` if an interaction with the repository fails. This should happen with the `MemoryRepository` only in exceptional situations, but when implementing persisted repositories, this may happen more frequently and should therefore be considered. +- The `MQTTClient::connect()` method had a parameter called `$sendCleanSessionFlag` while the `MqttClient::connect()` method has the same parameter, but called `$useCleanSession`. The parameters `$username` and `$password` have been removed entirely and are now part of the `ConnectionSettings`. +- The method `MQTTClient::close()` has been renamed to `MqttClient::disconnect()`. +- The parameter `$topic` of `MQTTClient::subscribe()` has been renamed to `$topicFilter` to reflect its meaning (which is a topic, but with wildcards). The `$callback` parameter can be `null` now and has `null` as default. +- The parameter `$topic` of `MQTTClient::unsubscribe()` has been renamed to `$topicFilter` as well. + +#### Exceptions + +- New exceptions have been introduced and old ones were removed. All exceptions inherit from `\PhpMqtt\Client\Exceptions\MqttClientException` as base. You should ensure your calls to methods of the `MqttClient` handle the exceptions appropriately. +- The exception constants previously defined on the `\PhpMqtt\Client\MQTTClient` class have been moved to the respective exception classes. This change only affects you if you used these constants to render detailed exception information for your users. + +#### Repositories + +- The `\PhpMqtt\Client\Contracts\Repository` interface has been changed significantly and summarizing all changes would be quite hard anyway. We therefore encourage you to have a look at the interface again and update your own implementation(s) of it, if you have any. + +#### Logger + +- The `\PhpMqtt\Client\Logger` implementation of `Psr\Log\LoggerInterface` does now decorate the log output with details about the MQTT client (format: `MQTT [{host}:{port}] [{clientId}] {message}`). + +### Additions + +- It is now possible to register event handlers for received messages. In combination with subscriptions without a callback, this allows to use centralized logic for multiple subscriptions. It also can be used for centralized logging, for example. +- A lot of unit and integration tests have been added which cover most parts of the library, especially the non-exception paths. +- All unit tests, integration tests, and the code style are enforced using a GitHub Actions workflow which runs under Ubuntu against multiple MQTT brokers (currently Mosquitto, HiveMQ and EMQ X). Contributing became easier therefore, but we expect that tests are added for changes and additions. + - To run the tests locally, an MQTT broker without authorization needs to run at `localhost:1883` (or the configuration in `phpunit.xml` is changed instead). +- The project is now analyzed using [sonarcloud.io](https://sonarcloud.io/dashboard?id=php-mqtt_client) which helps us keep up the high standards of the library. + +#### Methods + +- `MqttClient::isConnected()`: returns `true` if a connection is established (socket opened), and `false` otherwise. +- `MqttClient::getReceivedBytes()`: returns the number of raw bytes received from the broker (this includes meta information and not only message contents). +- `MqttClient::getSentBytes()`: returns the number of raw bytes sent to the broker (this includes meta information and not only message contents). + +### Removals + +_No functionality has been removed in this version._ diff --git a/vendor/php-mqtt/client/LICENSE.md b/vendor/php-mqtt/client/LICENSE.md new file mode 100644 index 0000000..0732640 --- /dev/null +++ b/vendor/php-mqtt/client/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Marvin Mall + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/php-mqtt/client/README.md b/vendor/php-mqtt/client/README.md new file mode 100644 index 0000000..3548bb6 --- /dev/null +++ b/vendor/php-mqtt/client/README.md @@ -0,0 +1,315 @@ +# php-mqtt/client + +[![Latest Stable Version](https://poser.pugx.org/php-mqtt/client/v)](https://packagist.org/packages/php-mqtt/client) +[![Total Downloads](https://poser.pugx.org/php-mqtt/client/downloads)](https://packagist.org/packages/php-mqtt/client) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=coverage)](https://sonarcloud.io/dashboard?id=php-mqtt_client) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=alert_status)](https://sonarcloud.io/dashboard?id=php-mqtt_client) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=security_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=php-mqtt_client) +[![License](https://poser.pugx.org/php-mqtt/client/license)](https://packagist.org/packages/php-mqtt/client) + +[`php-mqtt/client`](https://packagist.org/packages/php-mqtt/client) was created by, and is maintained +by [Marvin Mall](https://github.com/namoshek). +It allows you to connect to an MQTT broker where you can publish messages and subscribe to topics. +The current implementation supports all QoS levels ([with limitations](#limitations)). + +## Installation + +The package is available on [packagist.org](https://packagist.org/packages/php-mqtt/client) and can be installed using `composer`: + +```bash +composer require php-mqtt/client +``` + +The package requires PHP version 7.4 or higher. + +## Usage + +In the following, only a few very basic examples are given. For more elaborate examples, have a look at the +[`php-mqtt/client-examples` repository](https://github.com/php-mqtt/client-examples). + +### Publish + +A very basic publish example using QoS 0 requires only three steps: connect, publish and disconnect + +```php +$server = 'some-broker.example.com'; +$port = 1883; +$clientId = 'test-publisher'; + +$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId); +$mqtt->connect(); +$mqtt->publish('php-mqtt/client/test', 'Hello World!', 0); +$mqtt->disconnect(); +``` + +If you do not want to pass a `$clientId`, a random one will be generated for you. This will basically force a clean session implicitly. + +Be also aware that most of the methods can throw exceptions. The above example does not add any exception handling for brevity. + +### Subscribe + +Subscribing is a little more complex than publishing as it requires to run an event loop which reads, parses and handles messages from the broker: + +```php +$server = 'some-broker.example.com'; +$port = 1883; +$clientId = 'test-subscriber'; + +$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId); +$mqtt->connect(); +$mqtt->subscribe('php-mqtt/client/test', function ($topic, $message, $retained, $matchedWildcards) { + echo sprintf("Received message on topic [%s]: %s\n", $topic, $message); +}, 0); +$mqtt->loop(true); +$mqtt->disconnect(); +``` + +While the loop is active, you can use `$mqtt->interrupt()` to send an interrupt signal to the loop. +This will terminate the loop before it starts its next iteration. You can call this method using `pcntl_signal(SIGINT, $handler)` for example: + +```php +pcntl_async_signals(true); + +$clientId = 'test-subscriber'; + +$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId); +pcntl_signal(SIGINT, function (int $signal, $info) use ($mqtt) { + $mqtt->interrupt(); +}); +$mqtt->connect(); +$mqtt->subscribe('php-mqtt/client/test', function ($topic, $message, $retained, $matchedWildcards) { + echo sprintf("Received message on topic [%s]: %s\n", $topic, $message); +}, 0); +$mqtt->loop(true); +$mqtt->disconnect(); +``` + +### Client Settings + +As shown in the examples above, the `MqttClient` takes the server, port and client id as first, second and third parameter. +As fourth parameter, the protocol level can be passed. Currently supported is MQTT v3.1, +available as constant `MqttClient::MQTT_3_1`. +A fifth parameter allows passing a repository (currently, only a `MemoryRepository` is available by default). +Lastly, a logger can be passed as sixth parameter. If none is given, a null logger is used instead. + +Example: +```php +$mqtt = new \PhpMqtt\Client\MqttClient( + $server, + $port, + $clientId, + \PhpMqtt\Client\MqttClient::MQTT_3_1, + new \PhpMqtt\Client\Repositories\MemoryRepository(), + new Logger() +); +``` + +The `Logger` must implement the `Psr\Log\LoggerInterface`. + +### Connection Settings + +The `connect()` method of the `MqttClient` takes two optional parameters: +1. A `ConnectionSettings` instance +2. A `boolean` flag indicating whether a clean session should be requested (a random client id does this implicitly) + +Example: +```php +$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId); + +$connectionSettings = (new \PhpMqtt\Client\ConnectionSettings) + ->setConnectTimeout(3) + ->setUseTls(true) + ->setTlsSelfSignedAllowed(true); + +$mqtt->connect($connectionSettings, true); +``` + +The `ConnectionSettings` class provides a few settings through a fluent interface. The type itself is immutable, +and a new `ConnectionSettings` instance will be created for each added option. +This also prevents changes to the connection settings after a connection has been established. + +The following is a complete list of options with their respective default: + +```php +$connectionSettings = (new \PhpMqtt\Client\ConnectionSettings) + + // The username used for authentication when connecting to the broker. + ->setUsername(null) + + // The password used for authentication when connecting to the broker. + ->setPassword(null) + + // Whether to use a blocking socket when publishing messages or not. + // Normally, this setting can be ignored. When publishing large messages with multiple kilobytes in size, + // a blocking socket may be required if the receipt buffer of the broker is not large enough. + // + // Note: This setting has no effect on subscriptions, only on the publishing of messages. + ->useBlockingSocket(false) + + // The connect timeout defines the maximum amount of seconds the client will try to establish + // a socket connection with the broker. The value cannot be less than 1 second. + ->setConnectTimeout(60) + + // The socket timeout is the maximum amount of idle time in seconds for the socket connection. + // If no data is read or sent for the given amount of seconds, the socket will be closed. + // The value cannot be less than 1 second. + ->setSocketTimeout(5) + + // The resend timeout is the number of seconds the client will wait before sending a duplicate + // of pending messages without acknowledgement. The value cannot be less than 1 second. + ->setResendTimeout(10) + + // This flag determines whether the client will try to reconnect automatically + // if it notices a disconnect while sending data. + // The setting cannot be used together with the clean session flag. + ->setReconnectAutomatically(false) + + // Defines the maximum number of reconnect attempts until the client gives up. + // This setting is only relevant if setReconnectAutomatically() is set to true. + ->setMaxReconnectAttempts(3) + + // Defines the delay between reconnect attempts in milliseconds. + // This setting is only relevant if setReconnectAutomatically() is set to true. + ->setDelayBetweenReconnectAttempts(0) + + // The keep alive interval is the number of seconds the client will wait without sending a message + // until it sends a keep alive signal (ping) to the broker. The value cannot be less than 1 second + // and may not be higher than 65535 seconds. A reasonable value is 10 seconds (the default). + ->setKeepAliveInterval(10) + + // If the broker should publish a last will message in the name of the client when the client + // disconnects abruptly, this setting defines the topic on which the message will be published. + // + // A last will message will only be published if both this setting as well as the last will + // message are configured. + ->setLastWillTopic(null) + + // If the broker should publish a last will message in the name of the client when the client + // disconnects abruptly, this setting defines the message which will be published. + // + // A last will message will only be published if both this setting as well as the last will + // topic are configured. + ->setLastWillMessage(null) + + // The quality of service level the last will message of the client will be published with, + // if it gets triggered. + ->setLastWillQualityOfService(0) + + // This flag determines if the last will message of the client will be retained, if it gets + // triggered. Using this setting can be handy to signal that a client is offline by publishing + // a retained offline state in the last will and an online state as first message on connect. + ->setRetainLastWill(false) + + // This flag determines if TLS should be used for the connection. The port which is used to + // connect to the broker must support TLS connections. + ->setUseTls(false) + + // This flag determines if the peer certificate is verified, if TLS is used. + ->setTlsVerifyPeer(true) + + // This flag determines if the peer name is verified, if TLS is used. + ->setTlsVerifyPeerName(true) + + // This flag determines if self signed certificates of the peer should be accepted. + // Setting this to TRUE implies a security risk and should be avoided for production + // scenarios and public services. + ->setTlsSelfSignedAllowed(false) + + // The path to a Certificate Authority certificate which is used to verify the peer + // certificate, if TLS is used. + ->setTlsCertificateAuthorityFile(null) + + // The path to a directory containing Certificate Authority certificates which are + // used to verify the peer certificate, if TLS is used. + ->setTlsCertificateAuthorityPath(null) + + // The path to a client certificate file used for authentication, if TLS is used. + // + // The client certificate must be PEM encoded. It may optionally contain the + // certificate chain of issuers. + ->setTlsClientCertificateFile(null) + + // The path to a client certificate key file used for authentication, if TLS is used. + // + // This option requires ConnectionSettings::setTlsClientCertificateFile() to be used as well. + ->setTlsClientCertificateKeyFile(null) + + // The passphrase used to decrypt the private key of the client certificate, + // which in return is used for authentication, if TLS is used. + // + // This option requires ConnectionSettings::setTlsClientCertificateFile() and + // ConnectionSettings::setTlsClientCertificateKeyFile() to be used as well. + ->setTlsClientCertificateKeyPassphrase(null); +``` + +## Features + +- Supported MQTT Versions + - [x] v3 (just don't use v3.1 features like username & password) + - [x] v3.1 + - [x] v3.1.1 + - [ ] v5.0 +- Transport + - [x] TCP (unsecured) + - [x] TLS (secured, verifies the peer using a certificate authority file) +- Connect + - [x] Last Will + - [x] Message Retention + - [x] Authentication (username & password) + - [x] TLS encrypted connections + - [ ] Clean Session (can be set and sent, but the client has no persistence for QoS 2 messages) +- Publish + - [x] QoS Level 0 + - [x] QoS Level 1 (limitation: no persisted state across sessions) + - [x] QoS Level 2 (limitation: no persisted state across sessions) +- Subscribe + - [x] QoS Level 0 + - [x] QoS Level 1 + - [x] QoS Level 2 (limitation: no persisted state across sessions) +- Supported Message Length: unlimited _(no limits enforced, although the MQTT protocol supports only up to 256MB which one shouldn't use even remotely anyway)_ +- Logging possible (`Psr\Log\LoggerInterface` can be passed to the client) +- Persistence Drivers + - [x] In-Memory Driver + - [ ] Redis Driver + +## Limitations + +- Message flows with a QoS level higher than 0 are not persisted as the default implementation uses an in-memory repository for data. + To avoid issues with broken message flows, use the clean session flag to indicate that you don't care about old data. + It will not only instruct the broker to consider the connection new (without previous state), but will also reset the registered repository. + +## Developing & Testing + +### Certificates (TLS) + +To run the tests (especially the TLS tests), you will need to create certificates. A command has been provided for this: +```sh +sh create-certificates.sh +``` +This will create all required certificates in the `.ci/tls/` directory. The same script is used for continuous integration as well. + +### MQTT Broker for Testing + +Running the tests expects an MQTT broker to be running. The easiest way to run an MQTT broker is through Docker: +```sh +docker run --rm -it \ + -p 1883:1883 \ + -p 1884:1884 \ + -p 8883:8883 \ + -p 8884:8884 \ + -v $(pwd)/.ci/tls:/mosquitto-certs \ + -v $(pwd)/.ci/mosquitto.conf:/mosquitto/config/mosquitto.conf \ + -v $(pwd)/.ci/mosquitto.passwd:/mosquitto/config/mosquitto.passwd \ + eclipse-mosquitto:1.6 +``` +When run from the project directory, this will spawn a Mosquitto MQTT broker configured with the generated TLS certificates and a custom configuration. + +In case you intend to run a different broker or using a different method, or use a public broker instead, +you will need to adjust the environment variables defined in `phpunit.xml` accordingly. + +## License + +`php-mqtt/client` is open-sourced software licensed under the [MIT license](LICENSE.md). diff --git a/vendor/php-mqtt/client/composer.json b/vendor/php-mqtt/client/composer.json new file mode 100644 index 0000000..1c21492 --- /dev/null +++ b/vendor/php-mqtt/client/composer.json @@ -0,0 +1,53 @@ +{ + "name": "php-mqtt/client", + "description": "An MQTT client written in and for PHP.", + "type": "library", + "keywords": [ + "mqtt", + "client", + "publish", + "subscribe" + ], + "license": "MIT", + "authors": [ + { + "name": "Marvin Mall", + "email": "marvin-mall@msn.com", + "role": "developer" + } + ], + "autoload": { + "psr-4": { + "PhpMqtt\\Client\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "require": { + "php": "^8.0", + "psr/log": "^1.1|^2.0|^3.0", + "myclabs/php-enum": "^1.7" + }, + "require-dev": { + "phpunit/php-invoker": "^3.0", + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-redis": "Required for the RedisRepository" + }, + "scripts": { + "fix:cs": "vendor/bin/phpcbf", + "test": [ + "@test:cs", + "@test:all" + ], + "test:all": "vendor/bin/phpunit --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text", + "test:cs": "vendor/bin/phpcs", + "test:feature": "vendor/bin/phpunit --testsuite=Feature --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text", + "test:unit": "vendor/bin/phpunit --testsuite=Unit --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text" + } +} diff --git a/vendor/php-mqtt/client/create-certificates.sh b/vendor/php-mqtt/client/create-certificates.sh new file mode 100644 index 0000000..6cb6d36 --- /dev/null +++ b/vendor/php-mqtt/client/create-certificates.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# Generate a new CA certificate and key. +openssl genrsa -out .ci/tls/ca.key 2048 +openssl req -x509 -new -nodes -key .ci/tls/ca.key -days 1 -out .ci/tls/ca.crt -subj "/C=AT/ST=Vorarlberg/CN=php-mqtt Test CA" + +# Copy ca.crt to a file named by the hashed subject of the certificate. This is required for PHP's capath option to find the certificate. +cp .ci/tls/ca.crt .ci/tls/$(openssl x509 -hash -noout -in .ci/tls/ca.crt).0 + +# Create a Java Trust Store from the CA certificate. This is used by HiveMQ. +keytool -import -file .ci/tls/ca.crt -alias ca -keystore .ci/tls/ca.jks -storepass s3cr3t -trustcacerts -noprompt + +# Generate a new server certificate and key, signed by the created CA. +openssl genrsa -out .ci/tls/server.key 2048 +openssl req -new -key .ci/tls/server.key -out .ci/tls/server.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost" +openssl x509 -req -in .ci/tls/server.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/server.crt -days 1 -sha512 + +# Generate a Java Key Store from the server certificate. This is used by HiveMQ. +openssl pkcs12 -export -in .ci/tls/server.crt -inkey .ci/tls/server.key -out .ci/tls/server.p12 -passout pass:s3cr3t +keytool -importkeystore -srckeystore .ci/tls/server.p12 -srcstoretype PKCS12 -destkeystore .ci/tls/server.jks -deststoretype JKS -srcstorepass s3cr3t -deststorepass s3cr3t -noprompt + +# Generate a client certificate without passphrase, signed by the created CA. +openssl genrsa -out .ci/tls/client.key 2048 +openssl req -new -key .ci/tls/client.key -out .ci/tls/client.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost" +openssl x509 -req -in .ci/tls/client.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/client.crt -days 1 -sha256 + +# Generate a client certificate with passphrase, signed by the created CA. +openssl genrsa -aes128 -passout pass:s3cr3t -out .ci/tls/client2.key 2048 +openssl req -new -key .ci/tls/client2.key -passin pass:s3cr3t -out .ci/tls/client2.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost" +openssl x509 -req -in .ci/tls/client2.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/client2.crt -days 1 -sha256 diff --git a/vendor/php-mqtt/client/phpunit.xml b/vendor/php-mqtt/client/phpunit.xml new file mode 100644 index 0000000..989675f --- /dev/null +++ b/vendor/php-mqtt/client/phpunit.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + tests/Unit + + + tests/Feature + + + + + src + + + diff --git a/vendor/php-mqtt/client/sonar-project.properties b/vendor/php-mqtt/client/sonar-project.properties new file mode 100644 index 0000000..c78a448 --- /dev/null +++ b/vendor/php-mqtt/client/sonar-project.properties @@ -0,0 +1,18 @@ +sonar.organization=php-mqtt +sonar.projectKey=php-mqtt_client + +# Paths are relative to the sonar-project.properties file. +sonar.sources=src +sonar.tests=tests + +# Test report and code coverage related settings. +sonar.php.tests.reportPath=phpunit.report-junit.xml +sonar.php.coverage.reportPaths=phpunit.coverage-clover.xml + +# Encoding of the source code. Default is default system encoding. +sonar.sourceEncoding=UTF-8 + +# Links for sonarcloud.io page. +sonar.links.ci=https://github.com/php-mqtt/client/actions +sonar.links.scm=https://github.com/php-mqtt/client +sonar.links.issue=https://github.com/php-mqtt/client/issues diff --git a/vendor/php-mqtt/client/src/Concerns/GeneratesRandomClientIds.php b/vendor/php-mqtt/client/src/Concerns/GeneratesRandomClientIds.php new file mode 100644 index 0000000..3c2369b --- /dev/null +++ b/vendor/php-mqtt/client/src/Concerns/GeneratesRandomClientIds.php @@ -0,0 +1,21 @@ + */ + private $loopEventHandlers; + + /** @var \SplObjectStorage|array<\Closure> */ + private $publishEventHandlers; + + /** @var \SplObjectStorage|array<\Closure> */ + private $messageReceivedEventHandlers; + + /** @var \SplObjectStorage|array<\Closure> */ + private $connectedEventHandlers; + + /** + * Needs to be called in order to initialize the trait. + */ + protected function initializeEventHandlers(): void + { + $this->loopEventHandlers = new \SplObjectStorage(); + $this->publishEventHandlers = new \SplObjectStorage(); + $this->messageReceivedEventHandlers = new \SplObjectStorage(); + $this->connectedEventHandlers = new \SplObjectStorage(); + } + + /** + * Registers a loop event handler which is called each iteration of the loop. + * This event handler can be used for example to interrupt the loop under + * certain conditions. + * + * The loop event handler is passed the MQTT client instance as first and + * the elapsed time which the loop is already running for as second + * parameter. The elapsed time is a float containing seconds. + * + * Example: + * ```php + * $mqtt->registerLoopEventHandler(function ( + * MqttClient $mqtt, + * float $elapsedTime + * ) use ($logger) { + * $logger->info("Running for [{$elapsedTime}] seconds already."); + * }); + * ``` + * + * Multiple event handlers can be registered at the same time. + */ + public function registerLoopEventHandler(\Closure $callback): MqttClient + { + $this->loopEventHandlers->attach($callback); + + /** @var MqttClient $this */ + return $this; + } + + /** + * Unregisters a loop event handler which prevents it from being called + * in the future. + * + * This does not affect other registered event handlers. It is possible + * to unregister all registered event handlers by passing null as callback. + */ + public function unregisterLoopEventHandler(\Closure $callback = null): MqttClient + { + if ($callback === null) { + $this->loopEventHandlers->removeAll($this->loopEventHandlers); + } else { + $this->loopEventHandlers->detach($callback); + } + + /** @var MqttClient $this */ + return $this; + } + + /** + * Runs all registered loop event handlers with the given parameters. + * Each event handler is executed in a try-catch block to avoid spilling exceptions. + */ + private function runLoopEventHandlers(float $elapsedTime): void + { + foreach ($this->loopEventHandlers as $handler) { + try { + call_user_func($handler, $this, $elapsedTime); + } catch (\Throwable $e) { + $this->logger->error('Loop hook callback threw exception.', ['exception' => $e]); + } + } + } + + /** + * Registers a loop event handler which is called when a message is published. + * + * The loop event handler is passed the MQTT client as first, the topic as + * second and the message as third parameter. As fourth parameter, the message identifier + * will be passed, which can be null in case of QoS 0. The QoS level as well as the retained + * flag will also be passed as fifth and sixth parameters. + * + * Example: + * ```php + * $mqtt->registerPublishEventHandler(function ( + * MqttClient $mqtt, + * string $topic, + * string $message, + * ?int $messageId, + * int $qualityOfService, + * bool $retain + * ) use ($logger) { + * $logger->info("Sending message on topic [{$topic}]: {$message}"); + * }); + * ``` + * + * Multiple event handlers can be registered at the same time. + */ + public function registerPublishEventHandler(\Closure $callback): MqttClient + { + $this->publishEventHandlers->attach($callback); + + /** @var MqttClient $this */ + return $this; + } + + /** + * Unregisters a publish event handler which prevents it from being called + * in the future. + * + * This does not affect other registered event handlers. It is possible + * to unregister all registered event handlers by passing null as callback. + */ + public function unregisterPublishEventHandler(\Closure $callback = null): MqttClient + { + if ($callback === null) { + $this->publishEventHandlers->removeAll($this->publishEventHandlers); + } else { + $this->publishEventHandlers->detach($callback); + } + + /** @var MqttClient $this */ + return $this; + } + + /** + * Runs all the registered publish event handlers with the given parameters. + * Each event handler is executed in a try-catch block to avoid spilling exceptions. + */ + private function runPublishEventHandlers(string $topic, string $message, ?int $messageId, int $qualityOfService, bool $retain): void + { + foreach ($this->publishEventHandlers as $handler) { + try { + call_user_func($handler, $this, $topic, $message, $messageId, $qualityOfService, $retain); + } catch (\Throwable $e) { + $this->logger->error('Publish hook callback threw exception for published message on topic [{topic}].', [ + 'topic' => $topic, + 'exception' => $e, + ]); + } + } + } + + /** + * Registers an event handler which is called when a message is received from the broker. + * + * The message received event handler is passed the MQTT client as first, the topic as + * second and the message as third parameter. As fourth parameter, the QoS level will be + * passed and the retained flag as fifth. + * + * Example: + * ```php + * $mqtt->registerReceivedMessageEventHandler(function ( + * MqttClient $mqtt, + * string $topic, + * string $message, + * int $qualityOfService, + * bool $retained + * ) use ($logger) { + * $logger->info("Received message on topic [{$topic}]: {$message}"); + * }); + * ``` + * + * Multiple event handlers can be registered at the same time. + */ + public function registerMessageReceivedEventHandler(\Closure $callback): MqttClient + { + $this->messageReceivedEventHandlers->attach($callback); + + /** @var MqttClient $this */ + return $this; + } + + /** + * Unregisters a message received event handler which prevents it from being called in the future. + * + * This does not affect other registered event handlers. It is possible + * to unregister all registered event handlers by passing null as callback. + */ + public function unregisterMessageReceivedEventHandler(\Closure $callback = null): MqttClient + { + if ($callback === null) { + $this->messageReceivedEventHandlers->removeAll($this->messageReceivedEventHandlers); + } else { + $this->messageReceivedEventHandlers->detach($callback); + } + + /** @var MqttClient $this */ + return $this; + } + + /** + * Runs all the registered message received event handlers with the given parameters. + * Each event handler is executed in a try-catch block to avoid spilling exceptions. + */ + private function runMessageReceivedEventHandlers(string $topic, string $message, int $qualityOfService, bool $retained): void + { + foreach ($this->messageReceivedEventHandlers as $handler) { + try { + call_user_func($handler, $this, $topic, $message, $qualityOfService, $retained); + } catch (\Throwable $e) { + $this->logger->error('Received message hook callback threw exception for received message on topic [{topic}].', [ + 'topic' => $topic, + 'exception' => $e, + ]); + } + } + } + + /** + * Registers an event handler which is called when the client established a connection to the broker. + * This also includes manual reconnects as well as auto-reconnects by the client itself. + * + * The event handler is passed the MQTT client as first argument, + * followed by a flag which indicates whether an auto-reconnect occurred as second argument. + * + * Example: + * ```php + * $mqtt->registerConnectedEventHandler(function ( + * MqttClient $mqtt, + * bool $isAutoReconnect + * ) use ($logger) { + * if ($isAutoReconnect) { + * $logger->info("Client successfully auto-reconnected to the broker.); + * } else { + * $logger->info("Client successfully connected to the broker."); + * } + * }); + * ``` + * + * Multiple event handlers can be registered at the same time. + */ + public function registerConnectedEventHandler(\Closure $callback): MqttClient + { + $this->connectedEventHandlers->attach($callback); + + /** @var MqttClient $this */ + return $this; + } + + /** + * Unregisters a connected event handler which prevents it from being called in the future. + * + * This does not affect other registered event handlers. It is possible + * to unregister all registered event handlers by passing null as callback. + */ + public function unregisterConnectedEventHandler(\Closure $callback = null): MqttClient + { + if ($callback === null) { + $this->connectedEventHandlers->removeAll($this->connectedEventHandlers); + } else { + $this->connectedEventHandlers->detach($callback); + } + + /** @var MqttClient $this */ + return $this; + } + + /** + * Runs all the registered connected event handlers. + * Each event handler is executed in a try-catch block to avoid spilling exceptions. + */ + private function runConnectedEventHandlers(bool $isAutoReconnect): void + { + foreach ($this->connectedEventHandlers as $handler) { + try { + call_user_func($handler, $this, $isAutoReconnect); + } catch (\Throwable $e) { + $this->logger->error('Connected hook callback threw exception.', ['exception' => $e]); + } + } + } +} diff --git a/vendor/php-mqtt/client/src/Concerns/TranscodesData.php b/vendor/php-mqtt/client/src/Concerns/TranscodesData.php new file mode 100644 index 0000000..1c89f41 --- /dev/null +++ b/vendor/php-mqtt/client/src/Concerns/TranscodesData.php @@ -0,0 +1,78 @@ +> 8; + $lsb = $length % 256; + + return chr($msb) . chr($lsb) . $data; + } + + /** + * Converts the given string to a number, assuming it is an MSB encoded message id. + * MSB means preceding characters have higher value. + */ + protected function decodeMessageId(string $encodedMessageId): int + { + $length = strlen($encodedMessageId); + $result = 0; + + foreach (str_split($encodedMessageId) as $index => $char) { + $result += ord($char) << (($length - 1) * 8 - ($index * 8)); + } + + return $result; + } + + /** + * Encodes the given message identifier as string. + */ + protected function encodeMessageId(int $messageId): string + { + return chr($messageId >> 8) . chr($messageId % 256); + } + + /** + * Encodes the length of a message as string, so it can be transmitted + * over the wire. + */ + protected function encodeMessageLength(int $length): string + { + $result = ''; + + do { + $digit = $length % 128; + $length = $length >> 7; + + // if there are more digits to encode, set the top bit of this digit + if ($length > 0) { + $digit = ($digit | 0x80); + } + + $result .= chr($digit); + } while ($length > 0); + + return $result; + } +} diff --git a/vendor/php-mqtt/client/src/Concerns/ValidatesConfiguration.php b/vendor/php-mqtt/client/src/Concerns/ValidatesConfiguration.php new file mode 100644 index 0000000..8f62d23 --- /dev/null +++ b/vendor/php-mqtt/client/src/Concerns/ValidatesConfiguration.php @@ -0,0 +1,89 @@ +getConnectTimeout() < 1) { + throw new ConfigurationInvalidException('The connect timeout cannot be less than 1 second.'); + } + + if ($settings->getSocketTimeout() < 1) { + throw new ConfigurationInvalidException('The socket timeout cannot be less than 1 second.'); + } + + if ($settings->getResendTimeout() < 1) { + throw new ConfigurationInvalidException('The resend timeout cannot be less than 1 second.'); + } + + if ($settings->getKeepAliveInterval() < 1 || $settings->getKeepAliveInterval() > 65535) { + throw new ConfigurationInvalidException('The keep alive interval must be a value in the range of 1 to 65535 seconds.'); + } + + if ($settings->getMaxReconnectAttempts() < 1) { + throw new ConfigurationInvalidException('The maximum reconnect attempts cannot be fewer than 1.'); + } + + if ($settings->getDelayBetweenReconnectAttempts() < 0) { + throw new ConfigurationInvalidException('The delay between reconnect attempts cannot be lower than 0.'); + } + + if ($settings->getUsername() !== null && trim($settings->getUsername()) === '') { + throw new ConfigurationInvalidException('The username may not consist of white space only.'); + } + + if ($settings->getLastWillTopic() !== null && trim($settings->getLastWillTopic()) === '') { + throw new ConfigurationInvalidException('The last will topic may not consist of white space only.'); + } + + if ($settings->getLastWillQualityOfService() < MqttClient::QOS_AT_MOST_ONCE + || $settings->getLastWillQualityOfService() > MqttClient::QOS_EXACTLY_ONCE) { + throw new ConfigurationInvalidException('The QoS for the last will must be a value in the range of 0 to 2.'); + } + + if ($settings->getTlsCertificateAuthorityFile() !== null && !is_file($settings->getTlsCertificateAuthorityFile())) { + throw new ConfigurationInvalidException('The Certificate Authority file setting must contain the path to a regular file.'); + } + + if ($settings->getTlsCertificateAuthorityPath() !== null && !is_dir($settings->getTlsCertificateAuthorityPath())) { + throw new ConfigurationInvalidException('The Certificate Authority path setting must contain the path to a directory.'); + } + + if ($settings->getTlsClientCertificateFile() !== null && !is_file($settings->getTlsClientCertificateFile())) { + throw new ConfigurationInvalidException('The client certificate file setting must contain the path to a regular file.'); + } + + if ($settings->getTlsClientCertificateKeyFile() !== null && !is_file($settings->getTlsClientCertificateKeyFile())) { + throw new ConfigurationInvalidException('The client certificate key file setting must contain the path to a regular file.'); + } + + if ($settings->getTlsClientCertificateKeyFile() !== null && $settings->getTlsClientCertificateFile() === null) { + throw new ConfigurationInvalidException('Using a client certificate key file without certificate does not work.'); + } + + if ($settings->getTlsClientCertificateKeyPassphrase() !== null && $settings->getTlsClientCertificateKeyFile() === null) { + throw new ConfigurationInvalidException('Using a client certificate key passphrase without key file does not work.'); + } + } +} diff --git a/vendor/php-mqtt/client/src/Concerns/WorksWithBuffers.php b/vendor/php-mqtt/client/src/Concerns/WorksWithBuffers.php new file mode 100644 index 0000000..df29a4d --- /dev/null +++ b/vendor/php-mqtt/client/src/Concerns/WorksWithBuffers.php @@ -0,0 +1,26 @@ +username = $username; + + return $copy; + } + + public function getUsername(): ?string + { + return $this->username; + } + + /** + * The password used for authentication when connecting to the broker. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setPassword(?string $password): ConnectionSettings + { + $copy = clone $this; + + $copy->password = $password; + + return $copy; + } + + public function getPassword(): ?string + { + return $this->password; + } + + /** + * Whether to use a blocking socket when publishing messages or not. + * Normally, this setting can be ignored. When publishing large messages with multiple kilobytes in size, + * a blocking socket may be required if the receipt buffer of the broker is not large enough. + * + * Note: This setting has no effect on subscriptions, only on the publishing of messages. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function useBlockingSocket(bool $useBlockingSocket): ConnectionSettings + { + $copy = clone $this; + + $copy->useBlockingSocket = $useBlockingSocket; + + return $copy; + } + + public function shouldUseBlockingSocket(): bool + { + return $this->useBlockingSocket; + } + + /** + * The connect timeout is the maximum amount of seconds the client will try to establish + * a socket connection with the broker. The value cannot be less than 1 second. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setConnectTimeout(int $connectTimeout): ConnectionSettings + { + $copy = clone $this; + + $copy->connectTimeout = $connectTimeout; + + return $copy; + } + + public function getConnectTimeout(): int + { + return $this->connectTimeout; + } + + /** + * The socket timeout is the maximum amount of idle time in seconds for the socket connection. + * If no data is read or sent for the given amount of seconds, the socket will be closed. + * The value cannot be less than 1 second. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setSocketTimeout(int $socketTimeout): ConnectionSettings + { + $copy = clone $this; + + $copy->socketTimeout = $socketTimeout; + + return $copy; + } + + public function getSocketTimeout(): int + { + return $this->socketTimeout; + } + + /** + * The resend timeout is the number of seconds the client will wait before sending a duplicate + * of pending messages without acknowledgement. The value cannot be less than 1 second. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setResendTimeout(int $resendTimeout): ConnectionSettings + { + $copy = clone $this; + + $copy->resendTimeout = $resendTimeout; + + return $copy; + } + + public function getResendTimeout(): int + { + return $this->resendTimeout; + } + + /** + * The keep alive interval is the number of seconds the client will wait without sending a message + * until it sends a keep alive signal (ping) to the broker. The value cannot be less than 1 second + * and may not be higher than 65535 seconds. A reasonable value is 10 seconds (the default). + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setKeepAliveInterval(int $keepAliveInterval): ConnectionSettings + { + $copy = clone $this; + + $copy->keepAliveInterval = $keepAliveInterval; + + return $copy; + } + + public function getKeepAliveInterval(): int + { + return $this->keepAliveInterval; + } + + /** + * This flag determines whether the client will try to reconnect automatically, + * if it notices a disconnect while sending data. + * The setting cannot be used together with the clean session flag. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setReconnectAutomatically(bool $reconnectAutomatically): ConnectionSettings + { + $copy = clone $this; + + $copy->reconnectAutomatically = $reconnectAutomatically; + + return $copy; + } + + public function shouldReconnectAutomatically(): bool + { + return $this->reconnectAutomatically; + } + + /** + * Defines the maximum number of reconnect attempts until the client gives up. This setting + * is only relevant if {@see setReconnectAutomatically()} is set to true. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setMaxReconnectAttempts(int $maxReconnectAttempts): ConnectionSettings + { + $copy = clone $this; + + $copy->maxReconnectAttempts = $maxReconnectAttempts; + + return $copy; + } + + public function getMaxReconnectAttempts(): int + { + return $this->maxReconnectAttempts; + } + + /** + * Defines the delay between reconnect attempts in milliseconds. + * This setting is only relevant if {@see setReconnectAutomatically()} is set to true. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setDelayBetweenReconnectAttempts(int $delayBetweenReconnectAttempts): ConnectionSettings + { + $copy = clone $this; + + $copy->delayBetweenReconnectAttempts = $delayBetweenReconnectAttempts; + + return $copy; + } + + public function getDelayBetweenReconnectAttempts(): int + { + return $this->delayBetweenReconnectAttempts; + } + + /** + * If the broker should publish a last will message in the name of the client when the client + * disconnects abruptly, this setting defines the topic on which the message will be published. + * + * A last will message will only be published if both this setting as well as the last will + * message are configured. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setLastWillTopic(?string $lastWillTopic): ConnectionSettings + { + $copy = clone $this; + + $copy->lastWillTopic = $lastWillTopic; + + return $copy; + } + + public function getLastWillTopic(): ?string + { + return $this->lastWillTopic; + } + + /** + * If the broker should publish a last will message in the name of the client when the client + * disconnects abruptly, this setting defines the message which will be published. + * + * A last will message will only be published if both this setting as well as the last will + * topic are configured. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setLastWillMessage(?string $lastWillMessage): ConnectionSettings + { + $copy = clone $this; + + $copy->lastWillMessage = $lastWillMessage; + + return $copy; + } + + public function getLastWillMessage(): ?string + { + return $this->lastWillMessage; + } + + /** + * Determines whether the client has a last will. + */ + public function hasLastWill(): bool + { + return $this->lastWillTopic !== null && $this->lastWillMessage !== null; + } + + /** + * The quality of service level the last will message of the client will be published with, + * if it gets triggered. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setLastWillQualityOfService(int $lastWillQualityOfService): ConnectionSettings + { + $copy = clone $this; + + $copy->lastWillQualityOfService = $lastWillQualityOfService; + + return $copy; + } + + public function getLastWillQualityOfService(): int + { + return $this->lastWillQualityOfService; + } + + /** + * This flag determines if the last will message of the client will be retained, if it gets + * triggered. Using this setting can be handy to signal that a client is offline by publishing + * a retained offline state in the last will and an online state as first message on connect. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setRetainLastWill(bool $lastWillRetain): ConnectionSettings + { + $copy = clone $this; + + $copy->lastWillRetain = $lastWillRetain; + + return $copy; + } + + public function shouldRetainLastWill(): bool + { + return $this->lastWillRetain; + } + + /** + * This flag determines if TLS should be used for the connection. The port which is used to + * connect to the broker must support TLS connections. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setUseTls(bool $useTls): ConnectionSettings + { + $copy = clone $this; + + $copy->useTls = $useTls; + + return $copy; + } + + public function shouldUseTls(): bool + { + return $this->useTls; + } + + /** + * This flag determines if the peer certificate is verified, if TLS is used. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setTlsVerifyPeer(bool $tlsVerifyPeer): ConnectionSettings + { + $copy = clone $this; + + $copy->tlsVerifyPeer = $tlsVerifyPeer; + + return $copy; + } + + public function shouldTlsVerifyPeer(): bool + { + return $this->tlsVerifyPeer; + } + + /** + * This flag determines if the peer name is verified, if TLS is used. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setTlsVerifyPeerName(bool $tlsVerifyPeerName): ConnectionSettings + { + $copy = clone $this; + + $copy->tlsVerifyPeerName = $tlsVerifyPeerName; + + return $copy; + } + + public function shouldTlsVerifyPeerName(): bool + { + return $this->tlsVerifyPeerName; + } + + /** + * This flag determines if self signed certificates of the peer should be accepted. + * Setting this to TRUE implies a security risk and should be avoided for production + * scenarios and public services. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setTlsSelfSignedAllowed(bool $tlsSelfSignedAllowed): ConnectionSettings + { + $copy = clone $this; + + $copy->tlsSelfSignedAllowed = $tlsSelfSignedAllowed; + + return $copy; + } + + public function isTlsSelfSignedAllowed(): bool + { + return $this->tlsSelfSignedAllowed; + } + + /** + * The path to a Certificate Authority certificate which is used to verify the peer + * certificate, if TLS is used. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setTlsCertificateAuthorityFile(?string $tlsCertificateAuthorityFile): ConnectionSettings + { + $copy = clone $this; + + $copy->tlsCertificateAuthorityFile = $tlsCertificateAuthorityFile; + + return $copy; + } + + public function getTlsCertificateAuthorityFile(): ?string + { + return $this->tlsCertificateAuthorityFile; + } + + /** + * The path to a directory containing Certificate Authority certificates which are + * used to verify the peer certificate, if TLS is used. + * + * Certificate files in this directory must be named by the hash of the certificate, + * ending with ".0" (without quotes). The certificate hash can be retrieved using the + * openssl_x509_parse() function, which returns an array. The hash can be found in the + * array under the key "hash". + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setTlsCertificateAuthorityPath(?string $tlsCertificateAuthorityPath): ConnectionSettings + { + $copy = clone $this; + + $copy->tlsCertificateAuthorityPath = $tlsCertificateAuthorityPath; + + return $copy; + } + + public function getTlsCertificateAuthorityPath(): ?string + { + return $this->tlsCertificateAuthorityPath; + } + + /** + * The path to a client certificate file used for authentication, if TLS is used. + * + * The client certificate must be PEM encoded. It may optionally contain the + * certificate chain of issuers. The certificate key can be included in this certificate + * file or in a separate file ({@see ConnectionSettings::setTlsClientCertificateKeyFile()}). + * A passphrase can be configured using {@see ConnectionSettings::setTlsClientCertificateKeyPassphrase()}. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setTlsClientCertificateFile(?string $tlsClientCertificateFile): ConnectionSettings + { + $copy = clone $this; + + $copy->tlsClientCertificateFile = $tlsClientCertificateFile; + + return $copy; + } + + public function getTlsClientCertificateFile(): ?string + { + return $this->tlsClientCertificateFile; + } + + /** + * The path to a client certificate key file used for authentication, if TLS is used. + * + * This option requires {@see ConnectionSettings::setTlsClientCertificateFile()} + * to be used as well. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setTlsClientCertificateKeyFile(?string $tlsClientCertificateKeyFile): ConnectionSettings + { + $copy = clone $this; + + $copy->tlsClientCertificateKeyFile = $tlsClientCertificateKeyFile; + + return $copy; + } + + public function getTlsClientCertificateKeyFile(): ?string + { + return $this->tlsClientCertificateKeyFile; + } + + /** + * The passphrase used to decrypt the private key of the client certificate, + * which in return is used for authentication, if TLS is used. + * + * This option requires {@see ConnectionSettings::setTlsClientCertificateFile()} + * and {@see ConnectionSettings::setTlsClientCertificateKeyFile()} to be used as well. + * + * Please be aware that your passphrase is not stored in secure memory when using this option. + * + * @return ConnectionSettings A copy of the original object with the new setting applied. + */ + public function setTlsClientCertificateKeyPassphrase(?string $tlsClientCertificateKeyPassphrase): ConnectionSettings + { + $copy = clone $this; + + $copy->tlsClientCertificateKeyPassphrase = $tlsClientCertificateKeyPassphrase; + + return $copy; + } + + public function getTlsClientCertificateKeyPassphrase(): ?string + { + return $this->tlsClientCertificateKeyPassphrase; + } +} diff --git a/vendor/php-mqtt/client/src/Contracts/MessageProcessor.php b/vendor/php-mqtt/client/src/Contracts/MessageProcessor.php new file mode 100644 index 0000000..ee1611d --- /dev/null +++ b/vendor/php-mqtt/client/src/Contracts/MessageProcessor.php @@ -0,0 +1,118 @@ +subscribe( + * '/foo/bar/+', + * function (string $topic, string $message, bool $retained, array $matchedWildcards) use ($logger) { + * $logger->info("Received {retained} message on topic [{topic}]: {message}", [ + * 'topic' => $topic, + * 'message' => $message, + * 'retained' => $retained ? 'retained' : 'live' + * ]); + * } + * ); + * ``` + * + * If no callback is passed, a subscription will still be made. Received messages are delivered only to + * event handlers for received messages though. + * + * @throws DataTransferException + * @throws RepositoryException + */ + public function subscribe(string $topicFilter, ?callable $callback = null, int $qualityOfService = 0): void; + + /** + * Unsubscribe from the given topic. + * + * @throws DataTransferException + * @throws RepositoryException + */ + public function unsubscribe(string $topicFilter): void; + + /** + * Sets the interrupted signal. Doing so instructs the client to exit the loop, if it is + * actually looping. + * + * Sending multiple interrupt signals has no effect, unless the client exits the loop, + * which resets the signal for another loop. + */ + public function interrupt(): void; + + /** + * Runs an event loop that handles messages from the server and calls the registered + * callbacks for published messages. + * + * If the second parameter is provided, the loop will exit as soon as all + * queues are empty. This means there may be no open subscriptions, + * no pending messages as well as acknowledgments and no pending unsubscribe requests. + * + * The third parameter will, if set, lead to a forceful exit after the specified + * amount of seconds, but only if the second parameter is set to true. This basically + * means that if we wait for all pending messages to be acknowledged, we only wait + * a maximum of $queueWaitLimit seconds until we give up. We do not exit after the + * given amount of time if there are open topic subscriptions though. + * + * @throws DataTransferException + * @throws InvalidMessageException + * @throws MqttClientException + * @throws ProtocolViolationException + */ + public function loop(bool $allowSleep = true, bool $exitWhenQueuesEmpty = false, ?int $queueWaitLimit = null): void; + + /** + * Runs an event loop iteration that handles messages from the server and calls the registered + * callbacks for published messages. Also resends pending messages and calls loop event handlers. + * + * This method can be used to integrate the MQTT client in another event loop (like ReactPHP or Ratchet). + * + * Note: To ensure the event handlers called by this method will receive the correct elapsed time, + * the caller is responsible to provide the correct starting time of the loop as returned by `microtime(true)`. + * + * @throws DataTransferException + * @throws InvalidMessageException + * @throws MqttClientException + * @throws ProtocolViolationException + */ + public function loopOnce(float $loopStartedAt, bool $allowSleep = false, int $sleepMicroseconds = 100000): void; + + /** + * Returns the host used by the client to connect to. + */ + public function getHost(): string; + + /** + * Returns the port used by the client to connect to. + */ + public function getPort(): int; + + /** + * Returns the identifier used by the client. + */ + public function getClientId(): string; + + /** + * Returns the total number of received bytes, across reconnects. + */ + public function getReceivedBytes(): int; + + /** + * Returns the total number of sent bytes, across reconnects. + */ + public function getSentBytes(): int; + + /** + * Registers a loop event handler which is called each iteration of the loop. + * This event handler can be used for example to interrupt the loop under + * certain conditions. + * + * The loop event handler is passed the MQTT client instance as first and + * the elapsed time which the loop is already running for as second + * parameter. The elapsed time is a float containing seconds. + * + * Example: + * ```php + * $mqtt->registerLoopEventHandler(function ( + * MqttClient $mqtt, + * float $elapsedTime + * ) use ($logger) { + * $logger->info("Running for [{$elapsedTime}] seconds already."); + * }); + * ``` + * + * Multiple event handlers can be registered at the same time. + */ + public function registerLoopEventHandler(\Closure $callback): MqttClient; + + /** + * Unregisters a loop event handler which prevents it from being called + * in the future. + * + * This does not affect other registered event handlers. It is possible + * to unregister all registered event handlers by passing null as callback. + */ + public function unregisterLoopEventHandler(\Closure $callback = null): MqttClient; + + /** + * Registers a loop event handler which is called when a message is published. + * + * The loop event handler is passed the MQTT client as first, the topic as + * second and the message as third parameter. As fourth parameter, the + * message identifier will be passed. The QoS level as well as the retained + * flag will also be passed as fifth and sixth parameters. + * + * Example: + * ```php + * $mqtt->registerPublishEventHandler(function ( + * MqttClient $mqtt, + * string $topic, + * string $message, + * int $messageId, + * int $qualityOfService, + * bool $retain + * ) use ($logger) { + * $logger->info("Received message on topic [{$topic}]: {$message}"); + * }); + * ``` + * + * Multiple event handlers can be registered at the same time. + */ + public function registerPublishEventHandler(\Closure $callback): MqttClient; + + /** + * Unregisters a publish event handler which prevents it from being called + * in the future. + * + * This does not affect other registered event handlers. It is possible + * to unregister all registered event handlers by passing null as callback. + */ + public function unregisterPublishEventHandler(\Closure $callback = null): MqttClient; + + /** + * Registers an event handler which is called when a message is received from the broker. + * + * The message received event handler is passed the MQTT client as first, the topic as + * second and the message as third parameter. As fourth parameter, the QoS level will be + * passed and the retained flag as fifth. + * + * Example: + * ```php + * $mqtt->registerReceivedMessageEventHandler(function ( + * MqttClient $mqtt, + * string $topic, + * string $message, + * int $qualityOfService, + * bool $retained + * ) use ($logger) { + * $logger->info("Received message on topic [{$topic}]: {$message}"); + * }); + * ``` + * + * Multiple event handlers can be registered at the same time. + */ + public function registerMessageReceivedEventHandler(\Closure $callback): MqttClient; + + /** + * Unregisters a message received event handler which prevents it from being called in the future. + * + * This does not affect other registered event handlers. It is possible + * to unregister all registered event handlers by passing null as callback. + */ + public function unregisterMessageReceivedEventHandler(\Closure $callback = null): MqttClient; +} diff --git a/vendor/php-mqtt/client/src/Contracts/Repository.php b/vendor/php-mqtt/client/src/Contracts/Repository.php new file mode 100644 index 0000000..6335e28 --- /dev/null +++ b/vendor/php-mqtt/client/src/Contracts/Repository.php @@ -0,0 +1,143 @@ +connectionErrorCode; + } + + /** + * Retrieves the connection error message. + */ + public function getConnectionErrorMessage(): ?string + { + return $this->connectionErrorMessage; + } +} diff --git a/vendor/php-mqtt/client/src/Exceptions/DataTransferException.php b/vendor/php-mqtt/client/src/Exceptions/DataTransferException.php new file mode 100644 index 0000000..68b6a13 --- /dev/null +++ b/vendor/php-mqtt/client/src/Exceptions/DataTransferException.php @@ -0,0 +1,27 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + */ + public function alert($message, array $context = []): void + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + */ + public function critical($message, array $context = []): void + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + */ + public function error($message, array $context = []): void + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + */ + public function warning($message, array $context = []): void + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + */ + public function notice($message, array $context = []): void + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + */ + public function info($message, array $context = []): void + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + */ + public function debug($message, array $context = []): void + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + */ + public function log($level, $message, array $context = []): void + { + if ($this->logger === null) { + return; + } + + $this->logger->log($level, $this->wrapLogMessage($message), $this->mergeContext($context)); + } + + /** + * Wraps the given log message by prepending the client id and broker. + */ + protected function wrapLogMessage(string $message): string + { + return 'MQTT [{host}:{port}] [{clientId}] ' . $message; + } + + /** + * Adds global context like host, port and client id to the log context. + */ + protected function mergeContext(array $context): array + { + return array_merge([ + 'host' => $this->host, + 'port' => $this->port, + 'clientId' => $this->clientId, + ], $context); + } +} diff --git a/vendor/php-mqtt/client/src/Message.php b/vendor/php-mqtt/client/src/Message.php new file mode 100644 index 0000000..b64cc1a --- /dev/null +++ b/vendor/php-mqtt/client/src/Message.php @@ -0,0 +1,105 @@ +type; + } + + public function getQualityOfService(): int + { + return $this->qualityOfService; + } + + public function getRetained(): bool + { + return $this->retained; + } + + public function getMessageId(): ?int + { + return $this->messageId; + } + + public function setMessageId(?int $messageId): Message + { + $this->messageId = $messageId; + + return $this; + } + + public function getTopic(): ?string + { + return $this->topic; + } + + public function setTopic(?string $topic): Message + { + $this->topic = $topic; + + return $this; + } + + public function getContent(): ?string + { + return $this->content; + } + + public function setContent(?string $content): Message + { + $this->content = $content; + + return $this; + } + + /** + * @return int[] + */ + public function getAcknowledgedQualityOfServices(): array + { + return $this->acknowledgedQualityOfServices; + } + + /** + * @param int[] $acknowledgedQualityOfServices + */ + public function setAcknowledgedQualityOfServices(array $acknowledgedQualityOfServices): Message + { + $this->acknowledgedQualityOfServices = $acknowledgedQualityOfServices; + + return $this; + } +} diff --git a/vendor/php-mqtt/client/src/MessageProcessors/BaseMessageProcessor.php b/vendor/php-mqtt/client/src/MessageProcessors/BaseMessageProcessor.php new file mode 100644 index 0000000..8a7acbc --- /dev/null +++ b/vendor/php-mqtt/client/src/MessageProcessors/BaseMessageProcessor.php @@ -0,0 +1,32 @@ +buildLengthPrefixedString('MQTT') . chr(0x04); // protocol version (4) + } + + /** + * {@inheritDoc} + */ + public function parseAndValidateMessage(string $message): ?Message + { + $result = parent::parseAndValidateMessage($message); + + if ($this->isPublishMessageWithNullCharacter($result)) { + throw new ProtocolViolationException('The broker sent us a message with the forbidden unicode character U+0000.'); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + protected function parseAndValidateSubscribeAcknowledgementMessage(string $data): Message + { + if (strlen($data) < 3) { + $this->logger->notice('Received invalid subscribe acknowledgement from the broker.'); + throw new InvalidMessageException('Received invalid subscribe acknowledgement from the broker.'); + } + + $messageId = $this->decodeMessageId($this->pop($data, 2)); + + // Parse and validate the QoS acknowledgements. + $acknowledgements = array_map('ord', str_split($data)); + foreach ($acknowledgements as $acknowledgement) { + if (!in_array($acknowledgement, [0, 1, 2, 128])) { + throw new InvalidMessageException('Received subscribe acknowledgement with invalid QoS values from the broker.'); + } + } + + return (new Message(MessageType::SUBSCRIBE_ACKNOWLEDGEMENT())) + ->setMessageId($messageId) + ->setAcknowledgedQualityOfServices($acknowledgements); + } + + /** + * Determines if the given message is a PUBLISH message and contains the unicode null character U+0000. + */ + private function isPublishMessageWithNullCharacter(?Message $message): bool + { + return $message !== null + && $message->getType()->equals(MessageType::PUBLISH()) + && $message->getContent() !== null + && preg_match('/\x{0000}/u', $message->getContent()); + } +} diff --git a/vendor/php-mqtt/client/src/MessageProcessors/Mqtt31MessageProcessor.php b/vendor/php-mqtt/client/src/MessageProcessors/Mqtt31MessageProcessor.php new file mode 100644 index 0000000..3da71a8 --- /dev/null +++ b/vendor/php-mqtt/client/src/MessageProcessors/Mqtt31MessageProcessor.php @@ -0,0 +1,712 @@ + $bufferLength) { + return false; + } + + // There can me a maximum of four bytes for the package length, which means we cann opt-out + // when reaching the 6th byte in the buffer. This is only a safety measure in case the broker + // is sending invalid messages. Normally, the loop exits on its own. + if ($byteIndex >= 6) { + break; + } + + // Otherwise, we can take seven bits to calculate the length and the remaining eighth bit + // as continuation bit. + $digit = ord($buffer[$byteIndex]); + $remainingLength += ($digit & 127) * $multiplier; + $multiplier *= 128; + $byteIndex++; + } while (($digit & 128) !== 0); + + // At this point, we can now tell whether the remaining length amount of bytes are available + // or not. If not, we return the amount of bytes required for the message to be complete. + $requiredBufferLength = $byteIndex + $remainingLength; + if ($requiredBufferLength > $bufferLength) { + $requiredBytes = $requiredBufferLength; + return false; + } + + // Now that we have a full message in the buffer, we can set the output and return. + $message = substr($buffer, 0, $requiredBufferLength); + return true; + } + + /** + * {@inheritDoc} + */ + public function buildConnectMessage(ConnectionSettings $connectionSettings, bool $useCleanSession = false): string + { + // The protocol name and version. + $buffer = $this->getEncodedProtocolNameAndVersion(); + + // Build connection flags based on the connection settings. + $buffer .= chr($this->buildConnectionFlags($connectionSettings, $useCleanSession)); + + // Encode and add the keep alive interval. + $buffer .= chr($connectionSettings->getKeepAliveInterval() >> 8); + $buffer .= chr($connectionSettings->getKeepAliveInterval() & 0xff); + + // Encode and add the client identifier. + $buffer .= $this->buildLengthPrefixedString($this->clientId); + + // Encode and add the last will topic and message, if configured. + if ($connectionSettings->hasLastWill()) { + $buffer .= $this->buildLengthPrefixedString($connectionSettings->getLastWillTopic()); + $buffer .= $this->buildLengthPrefixedString($connectionSettings->getLastWillMessage()); + } + + // Encode and add the credentials, if configured. + if ($connectionSettings->getUsername() !== null) { + $buffer .= $this->buildLengthPrefixedString($connectionSettings->getUsername()); + } + if ($connectionSettings->getPassword() !== null) { + $buffer .= $this->buildLengthPrefixedString($connectionSettings->getPassword()); + } + + // The header consists of the message type 0x10 and the length. + $header = chr(0x10) . $this->encodeMessageLength(strlen($buffer)); + + return $header . $buffer; + } + + /** + * Returns the encoded protocol name and version, ready to be sent as part of the CONNECT message. + */ + protected function getEncodedProtocolNameAndVersion(): string + { + return $this->buildLengthPrefixedString('MQIsdp') . chr(0x03); // protocol version (3) + } + + /** + * Builds the connection flags from the inputs and settings. + * + * The bit structure of the connection flags is as follows: + * 0 - reserved + * 1 - clean session flag + * 2 - last will flag + * 3 - QoS flag (1) + * 4 - QoS flag (2) + * 5 - retain last will flag + * 6 - password flag + * 7 - username flag + * + * @link http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#connect MQTT 3.1 Spec + */ + protected function buildConnectionFlags(ConnectionSettings $connectionSettings, bool $useCleanSession = false): int + { + $flags = 0; + + if ($useCleanSession) { + $this->logger->debug('Using the [clean session] flag for the connection.'); + $flags += 1 << 1; + } + + if ($connectionSettings->hasLastWill()) { + $this->logger->debug('Using the [will] flag for the connection.'); + $flags += 1 << 2; + + if ($connectionSettings->getLastWillQualityOfService() > self::QOS_AT_MOST_ONCE) { + $this->logger->debug('Using last will QoS level [{qos}] for the connection.', [ + 'qos' => $connectionSettings->getLastWillQualityOfService(), + ]); + $flags += $connectionSettings->getLastWillQualityOfService() << 3; + } + + if ($connectionSettings->shouldRetainLastWill()) { + $this->logger->debug('Using the [retain last will] flag for the connection.'); + $flags += 1 << 5; + } + } + + if ($connectionSettings->getPassword() !== null) { + $this->logger->debug('Using the [password] flag for the connection.'); + $flags += 1 << 6; + } + + if ($connectionSettings->getUsername() !== null) { + $this->logger->debug('Using the [username] flag for the connection.'); + $flags += 1 << 7; + } + + return $flags; + } + + /** + * {@inheritDoc} + */ + public function handleConnectAcknowledgement(string $message): void + { + if (strlen($message) !== 4 || ($messageType = ord($message[0]) >> 4) !== 2) { + $this->logger->error('Expected connect acknowledgement; received a different response.', ['messageType' => $messageType ?? null]); + + throw new ConnectingToBrokerFailedException( + ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_FAILED, + 'A connection could not be established. Expected connect acknowledgement; received a different response else.' + ); + } + + $errorCode = ord($message[3]); + $logContext = ['errorCode' => sprintf('0x%02X', $errorCode)]; + + switch ($errorCode) { + case 0x00: + $this->logger->info('Connection with broker established successfully.', $logContext); + break; + + case 0x01: + $this->logger->error('The broker does not support MQTT v3.1.', $logContext); + throw new ConnectingToBrokerFailedException( + ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_PROTOCOL_VERSION, + 'The configured broker does not support MQTT v3.1.' + ); + + case 0x02: + $this->logger->error('The broker rejected the sent identifier.', $logContext); + throw new ConnectingToBrokerFailedException( + ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_IDENTIFIER_REJECTED, + 'The configured broker rejected the sent identifier.' + ); + + case 0x03: + $this->logger->error('The broker is currently unavailable.', $logContext); + throw new ConnectingToBrokerFailedException( + ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_BROKER_UNAVAILABLE, + 'The configured broker is currently unavailable.' + ); + + case 0x04: + $this->logger->error('The broker reported the credentials as invalid.', $logContext); + throw new ConnectingToBrokerFailedException( + ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_INVALID_CREDENTIALS, + 'The configured broker reported the credentials as invalid.' + ); + + case 0x05: + $this->logger->error('The broker responded with unauthorized.', $logContext); + throw new ConnectingToBrokerFailedException( + ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_UNAUTHORIZED, + 'The configured broker responded with unauthorized.' + ); + + default: + $this->logger->error('The broker responded with an invalid error code [{errorCode}].', $logContext); + throw new ConnectingToBrokerFailedException( + ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_FAILED, + 'The configured broker responded with an invalid error code. A connection could not be established.' + ); + } + } + + /** + * Builds a ping request message. + */ + public function buildPingRequestMessage(): string + { + // The message consists of the command 0xc0 and the length 0. + return chr(0xc0) . chr(0x00); + } + + /** + * Builds a ping response message. + */ + public function buildPingResponseMessage(): string + { + // The message consists of the command 0xd0 and the length 0. + return chr(0xd0) . chr(0x00); + } + + /** + * Builds a disconnect message. + */ + public function buildDisconnectMessage(): string + { + // The message consists of the command 0xe0 and the length 0. + return chr(0xe0) . chr(0x00); + } + + /** + * {@inheritDoc} + */ + public function buildSubscribeMessage(int $messageId, array $subscriptions, bool $isDuplicate = false): string + { + // Encode the message id, it always consists of two bytes. + $buffer = $this->encodeMessageId($messageId); + + foreach ($subscriptions as $subscription) { + // Encode the topic as length prefixed string. + $buffer .= $this->buildLengthPrefixedString($subscription->getTopicFilter()); + + // Encode the quality of service level. + $buffer .= chr($subscription->getQualityOfServiceLevel()); + } + + // The header consists of the message type 0x82 and the length. + $header = chr(0x82) . $this->encodeMessageLength(strlen($buffer)); + + return $header . $buffer; + } + + /** + * {@inheritDoc} + */ + public function buildUnsubscribeMessage(int $messageId, array $topics, bool $isDuplicate = false): string + { + // Encode the message id, it always consists of two bytes. + $buffer = $this->encodeMessageId($messageId); + + foreach ($topics as $topic) { + // Encode the topic as length prefixed string. + $buffer .= $this->buildLengthPrefixedString($topic); + } + + // The header consists of the message type 0xa2 and the length. + // Additionally, the first byte may contain the duplicate flag. + $command = 0xa2 | ($isDuplicate ? 1 << 3 : 0); + $header = chr($command) . $this->encodeMessageLength(strlen($buffer)); + + return $header . $buffer; + } + + /** + * {@inheritDoc} + */ + public function buildPublishMessage( + string $topic, + string $message, + int $qualityOfService, + bool $retain, + int $messageId = null, + bool $isDuplicate = false, + ): string + { + // Encode the topic as length prefixed string. + $buffer = $this->buildLengthPrefixedString($topic); + + // Encode the message id, if given. It always consists of two bytes. + if ($messageId !== null) + { + $buffer .= $this->encodeMessageId($messageId); + } + + // Add the message without encoding. + $buffer .= $message; + + // Encode the command with supported flags. + $command = 0x30; + if ($retain) { + $command += 1 << 0; + } + if ($qualityOfService > self::QOS_AT_MOST_ONCE) { + $command += $qualityOfService << 1; + } + if ($qualityOfService > self::QOS_AT_MOST_ONCE && $isDuplicate) { + $command += 1 << 3; + } + + // Build the header from the command and the encoded message length. + $header = chr($command) . $this->encodeMessageLength(strlen($buffer)); + + return $header . $buffer; + } + + /** + * {@inheritDoc} + */ + public function buildPublishAcknowledgementMessage(int $messageId): string + { + return chr(0x40) . chr(0x02) . $this->encodeMessageId($messageId); + } + + /** + * {@inheritDoc} + */ + public function buildPublishReceivedMessage(int $messageId): string + { + return chr(0x50) . chr(0x02) . $this->encodeMessageId($messageId); + } + + /** + * {@inheritDoc} + */ + public function buildPublishReleaseMessage(int $messageId): string + { + return chr(0x62) . chr(0x02) . $this->encodeMessageId($messageId); + } + + /** + * {@inheritDoc} + */ + public function buildPublishCompleteMessage(int $messageId): string + { + return chr(0x70) . chr(0x02) . $this->encodeMessageId($messageId); + } + + /** + * {@inheritDoc} + */ + public function parseAndValidateMessage(string $message): ?Message + { + $qualityOfService = 0; + $retained = false; + $data = ''; + $result = $this->tryDecodeMessage($message, $command, $qualityOfService, $retained, $data); + + if ($result === false) { + throw new InvalidMessageException('The passed message could not be decoded.'); + } + + // Ensure the command is supported by this version of the protocol. + if ($command <= 0 || $command >= 15) { + $this->logger->error('Reserved command received from the broker. Supported are commands (including) 1-14.', [ + 'command' => $command, + ]); + throw new InvalidMessageException('A reserved command has been used in the message.'); + } + + // Then handle the command accordingly. + switch ($command) { + case 0x02: + throw new ProtocolViolationException('Unexpected connection acknowledgement.'); + + case 0x03: + return $this->parseAndValidatePublishMessage($data, $qualityOfService, $retained); + + case 0x04: + return $this->parseAndValidatePublishAcknowledgementMessage($data); + + case 0x05: + return $this->parseAndValidatePublishReceiptMessage($data); + + case 0x06: + return $this->parseAndValidatePublishReleaseMessage($data); + + case 0x07: + return $this->parseAndValidatePublishCompleteMessage($data); + + case 0x09: + return $this->parseAndValidateSubscribeAcknowledgementMessage($data); + + case 0x0b: + return $this->parseAndValidateUnsubscribeAcknowledgementMessage($data); + + case 0x0c: + return $this->parseAndValidatePingRequestMessage(); + + case 0x0d: + return $this->parseAndValidatePingAcknowledgementMessage(); + + default: + $this->logger->debug('Received message with unsupported command [{command}]. Skipping.', ['command' => $command]); + break; + } + + // If we arrive here, we must have parsed a message with an unsupported type, and it cannot be + // very relevant for us. So we return an empty result without information to skip processing. + return null; + } + + /** + * Attempt to decode the given message. If successful, the result is true and the reference + * parameters are set accordingly. Otherwise, false is returned and the reference parameters + * remain untouched. + */ + protected function tryDecodeMessage( + string $message, + ?int &$command = null, + ?int &$qualityOfService = null, + ?bool &$retained = null, + ?string &$data = null + ): bool + { + // If we received no input, we can return immediately without doing work. + if (strlen($message) === 0) { + return false; + } + + // If we received not at least the fixed header with one length indicating byte, + // we know that there can't be a valid message in the buffer. So we return early. + if (strlen($message) < 2) { + return false; + } + + // Read the first byte of a message (command and flags). + $byte = $message[0]; + $command = (int) (ord($byte) / 16); + $qualityOfService = (ord($byte) & 0x06) >> 1; + $retained = (bool) (ord($byte) & 0x01); + + // Read the second byte of a message (remaining length). + // If the continuation bit (8) is set on the length byte, another byte will be read as length. + $byteIndex = 1; + $remainingLength = 0; + $multiplier = 1; + do { + // If the buffer has no more data, but we need to read more for the length header, + // we cannot give useful information about the remaining length and exit early. + if ($byteIndex + 1 > strlen($message)) { + return false; + } + + // Otherwise, we can take seven bits to calculate the length and the remaining eighth bit + // as continuation bit. + $digit = ord($message[$byteIndex]); + $remainingLength += ($digit & 127) * $multiplier; + $multiplier *= 128; + $byteIndex++; + } while (($digit & 128) !== 0); + + // At this point, we can now tell whether the remaining length amount of bytes are available + // or not. If not, the message is incomplete. + $requiredBytes = $byteIndex + $remainingLength; + if ($requiredBytes > strlen($message)) { + return false; + } + + // Set the output data based on the calculated bytes. + $data = substr($message, $byteIndex, $remainingLength); + + return true; + } + + /** + * Parses a received published message. The data contains the whole message except the + * fixed header with command and length. The message structure is: + * + * [topic-length:topic:message]+ + */ + protected function parseAndValidatePublishMessage(string $data, int $qualityOfServiceLevel, bool $retained): ?Message + { + $topicLength = (ord($data[0]) << 8) + ord($data[1]); + $topic = substr($data, 2, $topicLength); + $content = substr($data, ($topicLength + 2)); + + $message = new Message(MessageType::PUBLISH(), $qualityOfServiceLevel, $retained); + + if ($qualityOfServiceLevel > self::QOS_AT_MOST_ONCE) { + if (strlen($content) < 2) { + $this->logger->error('Received a message with QoS level [{qos}] without message identifier. Waiting for retransmission.', [ + 'qos' => $qualityOfServiceLevel, + ]); + + // This message seems to be incomplete or damaged. We ignore it and wait for a retransmission, + // which will occur at some point due to QoS level > 0. + return null; + } + + // Publish messages with a quality of service level > 0 require acknowledgement and therefore + // also a message identifier. + $messageId = $this->decodeMessageId($this->pop($content, 2)); + $message->setMessageId($messageId); + } + + return $message + ->setTopic($topic) + ->setContent($content); + } + + /** + * Parses a received publish acknowledgement. The data contains the whole message except + * the fixed header with command and length. The message structure is: + * + * [message-identifier] + * + * @throws InvalidMessageException + */ + protected function parseAndValidatePublishAcknowledgementMessage(string $data): Message + { + if (strlen($data) !== 2) { + $this->logger->notice('Received invalid publish acknowledgement from the broker.'); + throw new InvalidMessageException('Received invalid publish acknowledgement from the broker.'); + } + + $messageId = $this->decodeMessageId($this->pop($data, 2)); + + return (new Message(MessageType::PUBLISH_ACKNOWLEDGEMENT())) + ->setMessageId($messageId); + } + + /** + * Parses a received publish receipt. The data contains the whole message except the + * fixed header with command and length. The message structure is: + * + * [message-identifier] + * + * @throws InvalidMessageException + */ + protected function parseAndValidatePublishReceiptMessage(string $data): Message + { + if (strlen($data) !== 2) { + $this->logger->notice('Received invalid publish receipt from the broker.'); + throw new InvalidMessageException('Received invalid publish receipt from the broker.'); + } + + $messageId = $this->decodeMessageId($this->pop($data, 2)); + + return (new Message(MessageType::PUBLISH_RECEIPT())) + ->setMessageId($messageId); + } + + /** + * Parses a received publish release message. The data contains the whole message except the + * fixed header with command and length. The message structure is: + * + * [message-identifier] + * + * @throws InvalidMessageException + */ + protected function parseAndValidatePublishReleaseMessage(string $data): Message + { + if (strlen($data) !== 2) { + $this->logger->notice('Received invalid publish release from the broker.'); + throw new InvalidMessageException('Received invalid publish release from the broker.'); + } + + $messageId = $this->decodeMessageId($this->pop($data, 2)); + + return (new Message(MessageType::PUBLISH_RELEASE())) + ->setMessageId($messageId); + } + + /** + * Parses a received publish confirmation message. The data contains the whole message except the + * fixed header with command and length. The message structure is: + * + * [message-identifier] + * + * @throws InvalidMessageException + */ + protected function parseAndValidatePublishCompleteMessage(string $data): Message + { + if (strlen($data) !== 2) { + $this->logger->notice('Received invalid publish complete from the broker.'); + throw new InvalidMessageException('Received invalid complete release from the broker.'); + } + + $messageId = $this->decodeMessageId($this->pop($data, 2)); + + return (new Message(MessageType::PUBLISH_COMPLETE())) + ->setMessageId($messageId); + } + + /** + * Parses a received subscription acknowledgement. The data contains the whole message except the + * fixed header with command and length. The message structure is: + * + * [message-identifier:[qos-level]+] + * + * The order of the received QoS levels matches the order of the sent subscriptions. + * + * @throws InvalidMessageException + */ + protected function parseAndValidateSubscribeAcknowledgementMessage(string $data): Message + { + if (strlen($data) < 3) { + $this->logger->notice('Received invalid subscribe acknowledgement from the broker.'); + throw new InvalidMessageException('Received invalid subscribe acknowledgement from the broker.'); + } + + $messageId = $this->decodeMessageId($this->pop($data, 2)); + + // Parse and validate the QoS acknowledgements. + $acknowledgements = array_map('ord', str_split($data)); + foreach ($acknowledgements as $acknowledgement) { + if (!in_array($acknowledgement, [0, 1, 2])) { + throw new InvalidMessageException('Received subscribe acknowledgement with invalid QoS values from the broker.'); + } + } + + return (new Message(MessageType::SUBSCRIBE_ACKNOWLEDGEMENT())) + ->setMessageId($messageId) + ->setAcknowledgedQualityOfServices($acknowledgements); + } + + /** + * Parses a received unsubscribe acknowledgement. The data contains the whole message except the + * fixed header with command and length. The message structure is: + * + * [message-identifier] + * + * @throws InvalidMessageException + */ + protected function parseAndValidateUnsubscribeAcknowledgementMessage(string $data): Message + { + if (strlen($data) !== 2) { + $this->logger->notice('Received invalid unsubscribe acknowledgement from the broker.'); + throw new InvalidMessageException('Received invalid unsubscribe acknowledgement from the broker.'); + } + + $messageId = $this->decodeMessageId($this->pop($data, 2)); + + return (new Message(MessageType::UNSUBSCRIBE_ACKNOWLEDGEMENT())) + ->setMessageId($messageId); + } + + /** + * Parses a received ping request. + */ + protected function parseAndValidatePingRequestMessage(): Message + { + return new Message(MessageType::PING_REQUEST()); + } + + /** + * Parses a received ping acknowledgement. + */ + protected function parseAndValidatePingAcknowledgementMessage(): Message + { + return new Message(MessageType::PING_RESPONSE()); + } +} diff --git a/vendor/php-mqtt/client/src/MessageType.php b/vendor/php-mqtt/client/src/MessageType.php new file mode 100644 index 0000000..289867a --- /dev/null +++ b/vendor/php-mqtt/client/src/MessageType.php @@ -0,0 +1,37 @@ +clientId = $clientId ?? $this->generateRandomClientId(); + $this->repository = $repository ?? new MemoryRepository(); + $this->logger = new Logger($this->host, $this->port, $this->clientId, $logger); + + $this->messageProcessor = match ($protocol) { + self::MQTT_3_1_1 => new Mqtt311MessageProcessor($this->clientId, $this->logger), + default => new Mqtt31MessageProcessor($this->clientId, $this->logger), + }; + + $this->initializeEventHandlers(); + } + + /** + * {@inheritDoc} + */ + public function connect(ConnectionSettings $settings = null, bool $useCleanSession = false): void + { + // Always abruptly close any previous connection if we are opening a new one. + // The caller should make sure this does not happen. + $this->closeSocket(); + + $this->logger->debug('Connecting to broker.'); + + $this->settings = $settings ?? new ConnectionSettings(); + + $this->ensureConnectionSettingsAreValid($this->settings); + + // Because a clean session would make reconnects inherently more complex since all subscriptions would need to be replayed after reconnecting, + // we simply do not allow using these two features together. + if ($useCleanSession && $this->settings->shouldReconnectAutomatically()) { + throw new ConfigurationInvalidException('Automatic reconnects cannot be used together with the clean session flag.'); + } + + // When a clean session is requested, we have to reset the repository to forget about persisted states. + if ($useCleanSession) { + $this->repository->reset(); + } + + $this->connectInternal($useCleanSession); + } + + /** + * Connect to the MQTT broker using the configured settings. + * + * @throws ConnectingToBrokerFailedException + */ + protected function connectInternal(bool $useCleanSession = false, bool $isAutoReconnect = false): void + { + try { + $this->establishSocketConnection(); + $this->performConnectionHandshake($useCleanSession); + } catch (ConnectingToBrokerFailedException $e) { + $this->closeSocket(); + + throw $e; + } + + $this->connected = true; + + $this->runConnectedEventHandlers($isAutoReconnect); + } + + /** + * Opens a socket that connects to the host and port set on the object. + * + * When this method is called, all connection settings have been validated. + * + * @throws ConnectingToBrokerFailedException + */ + protected function establishSocketConnection(): void + { + $contextOptions = []; + + // Only if TLS is enabled, we add all TLS options to the context options. + if ($this->settings->shouldUseTls()) { + $this->logger->debug('Using TLS for the connection to the broker.'); + + $shouldVerifyPeer = $this->settings->shouldTlsVerifyPeer() + || $this->settings->getTlsCertificateAuthorityFile() !== null + || $this->settings->getTlsCertificateAuthorityPath() !== null; + + if (!$shouldVerifyPeer) { + $this->logger->warning('Using TLS without peer verification is discouraged. Are you aware of the security risk?'); + } + + if ($this->settings->isTlsSelfSignedAllowed()) { + $this->logger->warning('Using TLS with self-signed certificates is discouraged. Please use a CA file to verify it.'); + } + + $tlsOptions = [ + 'verify_peer' => $shouldVerifyPeer, + 'verify_peer_name' => $this->settings->shouldTlsVerifyPeerName(), + 'allow_self_signed' => $this->settings->isTlsSelfSignedAllowed(), + ]; + + if ($this->settings->getTlsCertificateAuthorityFile() !== null) { + $tlsOptions['cafile'] = $this->settings->getTlsCertificateAuthorityFile(); + } + + if ($this->settings->getTlsCertificateAuthorityPath() !== null) { + $tlsOptions['capath'] = $this->settings->getTlsCertificateAuthorityPath(); + } + + if ($this->settings->getTlsClientCertificateFile() !== null) { + $tlsOptions['local_cert'] = $this->settings->getTlsClientCertificateFile(); + } + + if ($this->settings->getTlsClientCertificateKeyFile() !== null) { + $tlsOptions['local_pk'] = $this->settings->getTlsClientCertificateKeyFile(); + } + + if ($this->settings->getTlsClientCertificateKeyPassphrase() !== null) { + $tlsOptions['passphrase'] = $this->settings->getTlsClientCertificateKeyPassphrase(); + } + + $contextOptions['ssl'] = $tlsOptions; + } + + $connectionString = 'tcp://' . $this->getHost() . ':' . $this->getPort(); + $socketContext = stream_context_create($contextOptions); + + $socket = @stream_socket_client( + $connectionString, + $errorCode, + $errorMessage, + $this->settings->getConnectTimeout(), + STREAM_CLIENT_CONNECT, + $socketContext + ); + + // The socket will be set to false if stream_socket_client() returned an error. + if ($socket === false) { + $this->logger->error('Establishing a connection with the broker using the connection string [{connectionString}] failed: {error}', [ + 'connectionString' => $connectionString, + 'error' => $errorMessage, + 'code' => $errorCode, + ]); + throw new ConnectingToBrokerFailedException( + ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_SOCKET_ERROR, + sprintf('Socket error [%d]: %s', $errorCode, $errorMessage), + (string) $errorCode, + $errorMessage + ); + } + + // If TLS is enabled, we need to enable it on the already created stream. + // Until now, we only created a normal TCP stream. + if ($this->settings->shouldUseTls()) { + // Since stream_socket_enable_crypto() communicates errors using error_get_last(), + // we need to clear a potentially set error at this point to be sure the error we + // retrieve in the error handling part is actually of this function call and not + // from some unrelated code of the users application. + error_clear_last(); + + $this->logger->debug('Enabling TLS on the existing socket connection.'); + + $enableEncryptionResult = @stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_ANY_CLIENT); + + if ($enableEncryptionResult === false) { + // At this point, PHP should have given us something like this: + // SSL operation failed with code 1. OpenSSL Error messages: + // error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed + // We need to get our hands dirty and extract the OpenSSL error + // from the PHP message, which luckily gives us a handy newline. + $this->parseTlsErrorMessage(error_get_last(), $tlsErrorCode, $tlsErrorMessage); + + // Before returning an exception, we need to close the already opened socket. + @fclose($socket); + + $this->logger->error('Enabling TLS on the connection with the MQTT broker failed (code {errorCode}): {errorMessage}', [ + 'errorMessage' => $tlsErrorMessage, + 'errorCode' => $tlsErrorCode, + ]); + + throw new ConnectingToBrokerFailedException( + ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_TLS_ERROR, + sprintf('TLS error [%s]: %s', $tlsErrorCode, $tlsErrorMessage), + $tlsErrorCode, + $tlsErrorMessage + ); + } + + $this->logger->debug('TLS enabled successfully.'); + } + + stream_set_timeout($socket, $this->settings->getSocketTimeout()); + stream_set_blocking($socket, false); + + $this->logger->debug('Socket opened and ready to use.'); + + $this->socket = $socket; + } + + /** + * Internal parser for SSL-related PHP error messages. + */ + private function parseTlsErrorMessage(?array $phpError, ?string &$tlsErrorCode = null, ?string &$tlsErrorMessage = null): void + { + if (!$phpError || !isset($phpError['message'])) { + $tlsErrorCode = "UNKNOWN:1"; + $tlsErrorMessage = "Unknown error"; + return; + } + + if (!preg_match('/:\n(?:error:([0-9A-Z]+):)?(.+)$/', $phpError['message'], $matches)) { + $tlsErrorCode = "UNKNOWN:2"; + $tlsErrorMessage = $phpError['message']; + return; + } + + if ($matches[1] == "") { + $tlsErrorCode = "UNKNOWN:3"; + $tlsErrorMessage = $matches[2]; + return; + } + + $tlsErrorCode = $matches[1]; + $tlsErrorMessage = $matches[2]; + } + + /** + * Performs the connection handshake with the help of the configured message processor. + * The connection handshake is expected to have the same flow all the time: + * - Connect request with variable length + * - Connect acknowledgement with variable length + * + * @throws ConnectingToBrokerFailedException + */ + protected function performConnectionHandshake(bool $useCleanSession = false): void + { + try { + $connectionHandshakeStartedAt = microtime(true); + + $data = $this->messageProcessor->buildConnectMessage($this->settings, $useCleanSession); + + $this->logger->debug('Sending connection handshake to broker.'); + + $this->writeToSocket($data); + + // Start by waiting for the first byte, then using polling logic to fetch all remaining + // data from the socket. + $buffer = $this->readFromSocket(1); + $requiredBytes = -1; + while (true) { + if ($requiredBytes > 0) { + $buffer .= $this->readFromSocket($requiredBytes); + } else { + $buffer .= $this->readAllAvailableDataFromSocket(); + } + + $message = null; + $result = $this->messageProcessor->tryFindMessageInBuffer($buffer, strlen($buffer), $message, $requiredBytes); + + // We only need to wait for the bytes we don't have in the buffer yet. + $requiredBytes = $requiredBytes - strlen($buffer); + + if ($result === true) { + /** @var string $message */ + + // Remove the parsed data from the buffer. + $buffer = substr($buffer, strlen($message)); + + // Process the acknowledgement message. + $this->messageProcessor->handleConnectAcknowledgement($message); + + break; + } + + // If no acknowledgement has been received from the broker within the configured connection timeout period, + // we abort the connection attempt and assume broker unavailability. + if (microtime(true) - $this->settings->getConnectTimeout() > $connectionHandshakeStartedAt) { + throw new ConnectingToBrokerFailedException( + ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_BROKER_UNAVAILABLE, + 'The broker did not acknowledge the connection attempt within the configured connection timeout period.' + ); + } + } + + // We need to set the global buffer to the remaining data we might already have read. + $this->buffer = $buffer; + } catch (DataTransferException) { + $this->logger->error('While connecting to the broker, a transfer error occurred.'); + throw new ConnectingToBrokerFailedException( + ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_FAILED, + 'A connection could not be established due to data transfer issues.' + ); + } + } + + /** + * Attempts to reconnect to the broker. If a connection cannot be established within the configured number of retries, + * the last caught exception is thrown. + * + * @throws ConnectingToBrokerFailedException + */ + protected function reconnect(): void + { + $maxReconnectAttempts = $this->settings->getMaxReconnectAttempts(); + $delayBetweenReconnectAttempts = $this->settings->getDelayBetweenReconnectAttempts(); + + for ($i = 1; $i <= $maxReconnectAttempts; $i++) { + try { + $this->connectInternal(false, true); + + return; + } catch (ConnectingToBrokerFailedException $e) { + if ($i === $maxReconnectAttempts) { + throw $e; + } + + if ($delayBetweenReconnectAttempts > 0) { + usleep($delayBetweenReconnectAttempts * 1000); + } + } + } + } + + /** + * {@inheritDoc} + */ + public function interrupt(): void + { + $this->interrupted = true; + } + + /** + * {@inheritDoc} + */ + public function getHost(): string + { + return $this->host; + } + + /** + * {@inheritDoc} + */ + public function getPort(): int + { + return $this->port; + } + + /** + * {@inheritDoc} + */ + public function getClientId(): string + { + return $this->clientId; + } + + /** + * {@inheritDoc} + */ + public function getReceivedBytes(): int + { + return $this->bytesReceived; + } + + /** + * {@inheritDoc} + */ + public function getSentBytes(): int + { + return $this->bytesSent; + } + + /** + * {@inheritDoc} + */ + public function isConnected(): bool + { + return $this->connected; + } + + /** + * Ensures the client is connected to a broker (or at least thinks it is). + * This method does not account for closed sockets. + * + * @throws ClientNotConnectedToBrokerException + */ + protected function ensureConnected(): void + { + if (!$this->isConnected()) { + throw new ClientNotConnectedToBrokerException( + 'The client is not connected to a broker. The requested operation is impossible at this point.' + ); + } + } + + /** + * {@inheritDoc} + */ + public function disconnect(): void + { + $this->ensureConnected(); + + $this->sendDisconnect(); + + if ($this->socket !== null && is_resource($this->socket)) { + $this->logger->debug('Closing the socket to the broker.'); + + stream_socket_shutdown($this->socket, STREAM_SHUT_WR); + } + + $this->connected = false; + } + + /** + * {@inheritDoc} + */ + public function publish(string $topic, string $message, int $qualityOfService = 0, bool $retain = false): void + { + $this->ensureConnected(); + + $messageId = null; + + if ($qualityOfService > self::QOS_AT_MOST_ONCE) { + $messageId = $this->repository->newMessageId(); + + $pendingMessage = new PublishedMessage($messageId, $topic, $message, $qualityOfService, $retain); + $this->repository->addPendingOutgoingMessage($pendingMessage); + } + + $this->publishMessage($topic, $message, $qualityOfService, $retain, $messageId); + } + + /** + * Actually publishes a message after using the configured message processor to build it. + * This is an internal method used for both, initial publishing of messages as well as + * re-publishing in case of timeouts. + * + * @throws DataTransferException + */ + protected function publishMessage( + string $topic, + string $message, + int $qualityOfService, + bool $retain, + ?int $messageId = null, + bool $isDuplicate = false + ): void + { + $this->logger->debug('Publishing a message on topic [{topic}]: {message}', [ + 'topic' => $topic, + 'message' => $message, + 'qos' => $qualityOfService, + 'retain' => $retain, + 'messageId' => $messageId, + 'isDuplicate' => $isDuplicate, + ]); + + $this->runPublishEventHandlers($topic, $message, $messageId, $qualityOfService, $retain); + + $data = $this->messageProcessor->buildPublishMessage($topic, $message, $qualityOfService, $retain, $messageId, $isDuplicate); + + $this->writeToSocketWithAutoReconnect($data); + } + + /** + * {@inheritDoc} + */ + public function subscribe(string $topicFilter, callable $callback = null, int $qualityOfService = self::QOS_AT_MOST_ONCE): void + { + $this->ensureConnected(); + + $this->logger->debug('Subscribing to topic [{topicFilter}] with maximum QoS [{qos}].', [ + 'topicFilter' => $topicFilter, + 'qos' => $qualityOfService, + ]); + + $messageId = $this->repository->newMessageId(); + + // Create the subscription representation now, but it will become an + // actual subscription only upon acknowledgement from the broker. + $subscriptions = [new Subscription($topicFilter, $qualityOfService, $callback)]; + + $pendingMessage = new SubscribeRequest($messageId, $subscriptions); + $this->repository->addPendingOutgoingMessage($pendingMessage); + + $data = $this->messageProcessor->buildSubscribeMessage($messageId, $subscriptions); + $this->writeToSocketWithAutoReconnect($data); + } + + /** + * {@inheritDoc} + */ + public function unsubscribe(string $topicFilter): void + { + $this->ensureConnected(); + + $this->logger->debug('Unsubscribing from topic [{topicFilter}].', ['topicFilter' => $topicFilter]); + + $messageId = $this->repository->newMessageId(); + $topicFilters = [$topicFilter]; + + $pendingMessage = new UnsubscribeRequest($messageId, $topicFilters); + $this->repository->addPendingOutgoingMessage($pendingMessage); + + $data = $this->messageProcessor->buildUnsubscribeMessage($messageId, $topicFilters); + $this->writeToSocketWithAutoReconnect($data); + } + + /** + * Returns the next time the broker expects to be pinged. + */ + protected function nextPingAt(): float + { + return ($this->lastPingAt + $this->settings->getKeepAliveInterval()); + } + + /** + * {@inheritDoc} + */ + public function loop(bool $allowSleep = true, bool $exitWhenQueuesEmpty = false, int $queueWaitLimit = null): void + { + $this->logger->debug('Starting client loop to process incoming messages and the resend queue.'); + + $loopStartedAt = microtime(true); + + while (true) { + if ($this->interrupted) { + $this->interrupted = false; + break; + } + + $this->loopOnce($loopStartedAt, $allowSleep); + + // If configured, the loop is exited if all queues are empty or a certain time limit is reached (i.e. retry is aborted). + // In any case, there may not be any active subscriptions though. + if ($exitWhenQueuesEmpty && $this->repository->countSubscriptions() === 0) { + if ($this->allQueuesAreEmpty()) { + break; + } + + // The time limit is reached. This most likely means the outgoing queues could not be emptied in time. + // Probably the server did not respond with an acknowledgement. + if ($queueWaitLimit !== null && (microtime(true) - $loopStartedAt) > $queueWaitLimit) { + break; + } + } + } + } + + /** + * {@inheritDoc} + */ + public function loopOnce(float $loopStartedAt, bool $allowSleep = false, int $sleepMicroseconds = 100000): void + { + $elapsedTime = microtime(true) - $loopStartedAt; + $this->runLoopEventHandlers($elapsedTime); + + // Read data from the socket - as much as available. + $this->buffer .= $this->readAllAvailableDataFromSocket(true); + + // Try to parse a message from the buffer and handle it, as long as messages can be parsed. + if (strlen($this->buffer) > 0) { + $this->processMessageBuffer(); + } elseif ($allowSleep) { + usleep($sleepMicroseconds); + } + + // Republish messages expired without confirmation. + // This includes published messages, subscribe and unsubscribe requests. + $this->resendPendingMessages(); + + // If the last message of the broker has been received more seconds ago + // than specified by the keep alive time, we will send a ping to ensure + // the connection is kept alive. + if ($this->nextPingAt() <= microtime(true)) { + $this->ping(); + } + } + + /** + * Processes the incoming message buffer by parsing and handling the messages, until the buffer is empty. + * + * @throws DataTransferException + * @throws InvalidMessageException + * @throws MqttClientException + * @throws ProtocolViolationException + */ + private function processMessageBuffer(): void + { + while (true) { + $data = ''; + $requiredBytes = -1; + $hasMessage = $this->messageProcessor->tryFindMessageInBuffer($this->buffer, strlen($this->buffer), $data, $requiredBytes); + + // When there is no full message in the buffer, we stop processing for now and go on + // with the next iteration. + if ($hasMessage === false) { + break; + } + + // If we found a message, the buffer needs to be reduced by the message length. + $this->buffer = substr($this->buffer, strlen($data)); + + // We then pass the message over to the message processor to parse and validate it. + $message = $this->messageProcessor->parseAndValidateMessage($data); + + // The result is used by us to perform required actions according to the protocol. + if ($message !== null) { + $this->handleMessage($message); + } + } + } + + /** + * Handles the given message according to its contents. + * + * @throws DataTransferException + * @throws ProtocolViolationException + */ + protected function handleMessage(Message $message): void + { + // PUBLISH (incoming) + if ($message->getType()->equals(MessageType::PUBLISH())) { + if ($message->getQualityOfService() === self::QOS_AT_LEAST_ONCE) { + // QoS 1. + $this->sendPublishAcknowledgement($message->getMessageId()); + } + + if ($message->getQualityOfService() === self::QOS_EXACTLY_ONCE) { + // QoS 2, part 1. + try { + $pendingMessage = new PublishedMessage( + $message->getMessageId(), + $message->getTopic(), + $message->getContent(), + 2, + $message->getRetained() + ); + $this->repository->addPendingIncomingMessage($pendingMessage); + } catch (PendingMessageAlreadyExistsException) { + // We already received and processed this message. + } + + // Always acknowledge, even if we received it multiple times. + $this->sendPublishReceived($message->getMessageId()); + + // We only deliver this published message as soon as we receive a publish complete. + return; + } + + // For QoS 0 and QoS 1 we can deliver right away. + $this->deliverPublishedMessage($message->getTopic(), $message->getContent(), $message->getQualityOfService(), $message->getRetained()); + return; + } + + // PUBACK (outgoing, QoS 1) + // Receiving an acknowledgement allows us to remove the published message from the retry queue. + if ($message->getType()->equals(MessageType::PUBLISH_ACKNOWLEDGEMENT())) { + $result = $this->repository->removePendingOutgoingMessage($message->getMessageId()); + if ($result === false) { + $this->logger->notice('Received publish acknowledgement from the broker for already acknowledged message.', [ + 'messageId' => $message->getMessageId() + ]); + } + return; + } + + // PUBREC (outgoing, QoS 2, part 1) + // Receiving a receipt allows us to mark the published message as received. + if ($message->getType()->equals(MessageType::PUBLISH_RECEIPT())) { + try { + $result = $this->repository->markPendingOutgoingPublishedMessageAsReceived($message->getMessageId()); + } catch (PendingMessageNotFoundException) { + // This should never happen as we should have received all PUBREC messages before we see the first + // PUBCOMP which actually remove the message. So we do this for safety only. + $result = false; + } + if ($result === false) { + $this->logger->notice('Received publish receipt from the broker for already acknowledged message.', [ + 'messageId' => $message->getMessageId() + ]); + } + + // We always reply blindly to keep the flow moving. + $this->sendPublishRelease($message->getMessageId()); + return; + } + + // PUBREL (incoming, QoS 2, part 2) + // When the broker tells us we can release the received published message, we deliver it to subscribed callbacks. + if ($message->getType()->equals(MessageType::PUBLISH_RELEASE())) { + $pendingMessage = $this->repository->getPendingIncomingMessage($message->getMessageId()); + if (!$pendingMessage || !$pendingMessage instanceof PublishedMessage) { + $this->logger->notice('Received publish release from the broker for already released message.', [ + 'messageId' => $message->getMessageId(), + ]); + } else { + $this->deliverPublishedMessage( + $pendingMessage->getTopicName(), + $pendingMessage->getMessage(), + $pendingMessage->getQualityOfServiceLevel(), + $pendingMessage->wantsToBeRetained() + ); + + $this->repository->removePendingIncomingMessage($message->getMessageId()); + } + + // Always reply with the PUBCOMP packet so it stops resending it. + $this->sendPublishComplete($message->getMessageId()); + return; + } + + // PUBCOMP (outgoing, QoS 2 part 3) + // Receiving a completion allows us to remove a published message from the retry queue. + // At this point, the publish process is complete. + if ($message->getType()->equals(MessageType::PUBLISH_COMPLETE())) { + $result = $this->repository->removePendingOutgoingMessage($message->getMessageId()); + if ($result === false) { + $this->logger->notice('Received publish completion from the broker for already acknowledged message.', [ + 'messageId' => $message->getMessageId(), + ]); + } + return; + } + + // SUBACK + if ($message->getType()->equals(MessageType::SUBSCRIBE_ACKNOWLEDGEMENT())) { + $pendingMessage = $this->repository->getPendingOutgoingMessage($message->getMessageId()); + if (!$pendingMessage || !$pendingMessage instanceof SubscribeRequest) { + $this->logger->notice('Received subscribe acknowledgement from the broker for already acknowledged request.', [ + 'messageId' => $message->getMessageId(), + ]); + return; + } + + $acknowledgedSubscriptions = $pendingMessage->getSubscriptions(); + if (count($acknowledgedSubscriptions) != count($message->getAcknowledgedQualityOfServices())) { + throw new ProtocolViolationException(sprintf( + 'The MQTT broker responded with a different amount of QoS acknowledgements (%d) than we expected (%d).', + count($message->getAcknowledgedQualityOfServices()), + count($acknowledgedSubscriptions) + )); + } + + foreach ($message->getAcknowledgedQualityOfServices() as $index => $qualityOfService) { + // Starting from MQTT 3.1.1, the broker is able to reject individual subscriptions. + // Instead of failing the whole bulk, we log the incident and skip the single subscription. + if ($qualityOfService === 128) { + $this->logger->notice('The broker rejected the subscription to [{topicFilter}].', [ + 'topicFilter' => $acknowledgedSubscriptions[$index]->getTopicFilter(), + ]); + continue; + } + + // It may happen that the server registers our subscription + // with a lower quality of service than requested, in this + // case this is the one that we will record. + $acknowledgedSubscriptions[$index]->setQualityOfServiceLevel($qualityOfService); + + $this->repository->addSubscription($acknowledgedSubscriptions[$index]); + } + + $this->repository->removePendingOutgoingMessage($message->getMessageId()); + return; + } + + // UNSUBACK + if ($message->getType()->equals(MessageType::UNSUBSCRIBE_ACKNOWLEDGEMENT())) { + $pendingMessage = $this->repository->getPendingOutgoingMessage($message->getMessageId()); + if (!$pendingMessage || !$pendingMessage instanceof UnsubscribeRequest) { + $this->logger->notice('Received unsubscribe acknowledgement from the broker for already acknowledged request.', [ + 'messageId' => $message->getMessageId(), + ]); + return; + } + + foreach ($pendingMessage->getTopicFilters() as $topicFilter) { + $this->repository->removeSubscription($topicFilter); + } + + $this->repository->removePendingOutgoingMessage($message->getMessageId()); + return; + } + + // PINGREQ + if ($message->getType()->equals(MessageType::PING_REQUEST())) { + // Respond with PINGRESP. + $this->writeToSocketWithAutoReconnect($this->messageProcessor->buildPingResponseMessage()); + return; + } + } + + /** + * Determines if all queues are empty. + */ + protected function allQueuesAreEmpty(): bool + { + return $this->repository->countPendingOutgoingMessages() === 0 && + $this->repository->countPendingIncomingMessages() === 0; + } + + /** + * Delivers a published message to subscribed callbacks. + */ + protected function deliverPublishedMessage(string $topic, string $message, int $qualityOfServiceLevel, bool $retained = false): void + { + $subscribers = $this->repository->getSubscriptionsMatchingTopic($topic); + + $this->logger->debug('Delivering message received on topic [{topic}] with QoS [{qos}] from the broker to [{subscribers}] subscribers.', [ + 'topic' => $topic, + 'message' => $message, + 'qos' => $qualityOfServiceLevel, + 'subscribers' => count($subscribers), + ]); + + foreach ($subscribers as $subscriber) { + if ($subscriber->getCallback() === null) { + continue; + } + + try { + call_user_func($subscriber->getCallback(), $topic, $message, $retained, $subscriber->getMatchedWildcards($topic)); + } catch (\Throwable $e) { + $this->logger->error('Subscriber callback threw exception for published message on topic [{topic}].', [ + 'topic' => $topic, + 'message' => $message, + 'exception' => $e, + ]); + } + } + + $this->runMessageReceivedEventHandlers($topic, $message, $qualityOfServiceLevel, $retained); + } + + /** + * Republishes pending messages. + * + * @throws DataTransferException + * @throws InvalidMessageException + */ + protected function resendPendingMessages(): void + { + /** @noinspection PhpUnhandledExceptionInspection */ + $dateTime = (new \DateTime())->sub(new \DateInterval('PT' . $this->settings->getResendTimeout() . 'S')); + $messages = $this->repository->getPendingOutgoingMessagesLastSentBefore($dateTime); + + foreach ($messages as $pendingMessage) { + if ($pendingMessage instanceof PublishedMessage) { + $this->logger->debug('Re-publishing pending message to the broker.', [ + 'messageId' => $pendingMessage->getMessageId(), + ]); + + $this->publishMessage( + $pendingMessage->getTopicName(), + $pendingMessage->getMessage(), + $pendingMessage->getQualityOfServiceLevel(), + $pendingMessage->wantsToBeRetained(), + $pendingMessage->getMessageId(), + true + ); + } elseif ($pendingMessage instanceof SubscribeRequest) { + $this->logger->debug('Re-sending pending subscribe request to the broker.', [ + 'messageId' => $pendingMessage->getMessageId(), + ]); + + $data = $this->messageProcessor->buildSubscribeMessage($pendingMessage->getMessageId(), $pendingMessage->getSubscriptions(), true); + $this->writeToSocketWithAutoReconnect($data); + } elseif ($pendingMessage instanceof UnsubscribeRequest) { + $this->logger->debug('Re-sending pending unsubscribe request to the broker.', [ + 'messageId' => $pendingMessage->getMessageId(), + ]); + + $data = $this->messageProcessor->buildUnsubscribeMessage($pendingMessage->getMessageId(), $pendingMessage->getTopicFilters(), true); + $this->writeToSocketWithAutoReconnect($data); + } else { + throw new InvalidMessageException('Unexpected message type encountered while resending pending messages.'); + } + + $pendingMessage->setLastSentAt(new \DateTime()); + $pendingMessage->incrementSendingAttempts(); + } + } + + /** + * Sends a publish acknowledgement for the given message identifier. + * + * @throws DataTransferException + */ + protected function sendPublishAcknowledgement(int $messageId): void + { + $this->logger->debug('Sending publish acknowledgement to the broker (message id: {messageId}).', ['messageId' => $messageId]); + + $this->writeToSocketWithAutoReconnect($this->messageProcessor->buildPublishAcknowledgementMessage($messageId)); + } + + /** + * Sends a publish received message for the given message identifier. + * + * @throws DataTransferException + */ + protected function sendPublishReceived(int $messageId): void + { + $this->logger->debug('Sending publish received message to the broker (message id: {messageId}).', ['messageId' => $messageId]); + + $this->writeToSocketWithAutoReconnect($this->messageProcessor->buildPublishReceivedMessage($messageId)); + } + + /** + * Sends a publish release message for the given message identifier. + * + * @throws DataTransferException + */ + protected function sendPublishRelease(int $messageId): void + { + $this->logger->debug('Sending publish release message to the broker (message id: {messageId}).', ['messageId' => $messageId]); + + $this->writeToSocketWithAutoReconnect($this->messageProcessor->buildPublishReleaseMessage($messageId)); + } + + /** + * Sends a publish complete message for the given message identifier. + * + * @throws DataTransferException + */ + protected function sendPublishComplete(int $messageId): void + { + $this->logger->debug('Sending publish complete message to the broker (message id: {messageId}).', ['messageId' => $messageId]); + + $this->writeToSocketWithAutoReconnect($this->messageProcessor->buildPublishCompleteMessage($messageId)); + } + + /** + * Sends a ping message to the broker to keep the connection alive. + * + * @throws DataTransferException + */ + protected function ping(): void + { + $this->logger->debug('Sending ping to the broker to keep the connection alive.'); + + $this->writeToSocketWithAutoReconnect($this->messageProcessor->buildPingRequestMessage()); + } + + /** + * Sends a disconnect message to the broker. Does not close the socket. + * + * @throws DataTransferException + */ + protected function sendDisconnect(): void + { + $data = $this->messageProcessor->buildDisconnectMessage(); + + $this->logger->debug('Sending disconnect package to the broker.'); + + $this->writeToSocketWithAutoReconnect($data); + } + + /** + * Writes some data to the socket. If a {@see $length} is given, and it is shorter + * than the data, only {@see $length} amount of bytes will be sent. + * If configured, this method will try to reconnect in case of transmission errors. + * + * @throws DataTransferException + */ + protected function writeToSocketWithAutoReconnect(string $data, ?int $length = null): void + { + try { + $this->writeToSocket($data, $length); + } catch (DataTransferException $e) { + if (!$this->settings->shouldReconnectAutomatically()) { + throw $e; + } + + try { + $this->reconnect(); + } catch (ConnectingToBrokerFailedException) { + $this->logger->error('Automatically reconnecting to the broker while writing data to the socket failed.'); + + // Throw the original exception. + throw $e; + } + + // Retry writing to the socket. If this fails again, the exception is thrown as-is. + $this->writeToSocket($data, $length); + } + } + + /** + * Writes some data to the socket. If a {@see $length} is given, and it is shorter + * than the data, only {@see $length} amount of bytes will be sent. + * + * @throws DataTransferException + */ + protected function writeToSocket(string $data, ?int $length = null): void + { + $calculatedLength = strlen($data); + $length = min($length ?? $calculatedLength, $calculatedLength); + + if ($this->settings->shouldUseBlockingSocket()) { + socket_set_blocking($this->socket, true); + } + + $result = @fwrite($this->socket, $data, $length); + + if ($this->settings->shouldUseBlockingSocket()) { + socket_set_blocking($this->socket, false); + } + + if ($result === false || $result !== $length) { + $this->logger->error('Sending data over the socket to the broker failed.'); + throw new DataTransferException( + DataTransferException::EXCEPTION_TX_DATA, + 'Sending data over the socket failed. Has it been closed?' + ); + } + + $this->bytesSent += $length; + + $this->logger->debug('Sent data over the socket: {data}', ['data' => $data]); + + // After writing successfully to the socket, the broker should have received a new message from us. + // Because we only need to send a ping if no other messages are delivered, we can safely reset the ping timer. + $this->lastPingAt = microtime(true); + } + + /** + * Reads data from the socket. If the second parameter {@see $withoutBlocking} is set to true, + * a maximum of {@see $limit} bytes will be read and returned. If {@see $withoutBlocking} is set to false, + * the method will wait until {@see $limit} bytes have been received. + * If configured, this method will try to reconnect in case of transmission errors. + * + * @throws DataTransferException + */ + protected function readFromSocketWithAutoReconnect(int $limit = self::SOCKET_READ_BUFFER_SIZE, bool $withoutBlocking = false): string + { + try { + return $this->readFromSocket($limit, $withoutBlocking); + } catch (DataTransferException $e) { + if (!$this->settings->shouldReconnectAutomatically()) { + throw $e; + } + + try { + $this->reconnect(); + } catch (ConnectingToBrokerFailedException) { + $this->logger->error('Automatically reconnecting to the broker while reading data from the socket failed.'); + + // Throw the original exception. + throw $e; + } + + // Retry writing to the socket. If this fails again, the exception is thrown as-is. + return $this->readFromSocket($limit, $withoutBlocking); + } + } + + /** + * Reads data from the socket. If the second parameter {@see $withoutBlocking} is set to true, + * a maximum of {@see $limit} bytes will be read and returned. If {@see $withoutBlocking} is set to false, + * the method will wait until {@see $limit} bytes have been received. + * + * @throws DataTransferException + */ + protected function readFromSocket(int $limit = self::SOCKET_READ_BUFFER_SIZE, bool $withoutBlocking = false): string + { + if ($withoutBlocking) { + $result = fread($this->socket, $limit); + + if ($result === false) { + $this->logger->error('Reading data from the socket of the broker failed.'); + throw new DataTransferException( + DataTransferException::EXCEPTION_RX_DATA, + 'Reading data from the socket failed. Has it been closed?' + ); + } + + $this->bytesReceived += strlen($result); + + $this->logger->debug('Read data from the socket (without blocking): {data}', ['data' => $result]); + + return $result; + } + + $result = ''; + $remaining = $limit; + + $this->logger->debug('Waiting for {bytes} bytes of data.', ['bytes' => $remaining]); + + while (feof($this->socket) === false && $remaining > 0) { + $receivedData = fread($this->socket, $remaining); + if ($receivedData === false) { + $this->logger->error('Reading data from the socket of the broker failed.'); + throw new DataTransferException( + DataTransferException::EXCEPTION_RX_DATA, + 'Reading data from the socket failed. Has it been closed?' + ); + } + $result .= $receivedData; + $remaining = $limit - strlen($result); + } + + $this->bytesReceived += strlen($result); + + $this->logger->debug('Read data from the socket: {data}', ['data' => $result]); + + return $result; + } + + /** + * Reads all the available data from the socket using non-blocking mode. Essentially this means + * that {@see MqttClient::readFromSocketWithAutoReconnect()} is called over and over again, as long as data is + * returned. + * + * @throws DataTransferException + */ + protected function readAllAvailableDataFromSocket(bool $withAutoReconnectIfConfigured = false): string + { + $result = ''; + + while (true) { + $buffer = ($withAutoReconnectIfConfigured && $this->settings->shouldReconnectAutomatically()) + ? $this->readFromSocketWithAutoReconnect(self::SOCKET_READ_BUFFER_SIZE, true) + : $this->readFromSocket(self::SOCKET_READ_BUFFER_SIZE, true); + + $result .= $buffer; + + if (strlen($buffer) < self::SOCKET_READ_BUFFER_SIZE) { + break; + } + } + + return $result; + } + + /** + * Closes the socket connection immediately, without flushing queued data. + */ + protected function closeSocket(): void + { + if ($this->socket === null || !is_resource($this->socket)) { + return; + } + + if (@fclose($this->socket)) { + $this->logger->debug('Successfully closed socket connection to the broker.'); + } else { + $phpError = error_get_last(); + $this->logger->notice('Closing socket connection failed: {error}', [ + 'error' => $phpError ? $phpError['message'] : 'undefined', + ]); + } + + $this->socket = null; + } +} diff --git a/vendor/php-mqtt/client/src/PendingMessage.php b/vendor/php-mqtt/client/src/PendingMessage.php new file mode 100644 index 0000000..f7eac8b --- /dev/null +++ b/vendor/php-mqtt/client/src/PendingMessage.php @@ -0,0 +1,76 @@ +lastSentAt = $sentAt ?? new DateTime(); + } + + /** + * Returns the message identifier. + */ + public function getMessageId(): int + { + return $this->messageId; + } + + /** + * Returns the date time when the message was last sent. + */ + public function getLastSentAt(): DateTime + { + return $this->lastSentAt; + } + + /** + * Returns the number of times the message has been sent. + */ + public function getSendingAttempts(): int + { + return $this->sendingAttempts; + } + + /** + * Sets the date time when the message was last sent. + */ + public function setLastSentAt(DateTime $value = null): self + { + $this->lastSentAt = $value ?? new DateTime(); + + return $this; + } + + /** + * Increments the sending attempts by one. + */ + public function incrementSendingAttempts(): self + { + $this->sendingAttempts++; + + return $this; + } +} diff --git a/vendor/php-mqtt/client/src/PublishedMessage.php b/vendor/php-mqtt/client/src/PublishedMessage.php new file mode 100644 index 0000000..c2edcdf --- /dev/null +++ b/vendor/php-mqtt/client/src/PublishedMessage.php @@ -0,0 +1,84 @@ +topicName; + } + + /** + * Returns the content of the published message. + */ + public function getMessage(): string + { + return $this->message; + } + + /** + * Returns the requested quality of service level. + */ + public function getQualityOfServiceLevel(): int + { + return $this->qualityOfService; + } + + /** + * Determines whether this message wants to be retained. + */ + public function wantsToBeRetained(): bool + { + return $this->retain; + } + + /** + * Determines whether the message has been confirmed as received. + */ + public function hasBeenReceived(): bool + { + return $this->received; + } + + /** + * Marks the published message as received (QoS level 2). + * + * Returns `true` if the message was not previously received. Otherwise `false` will be returned. + */ + public function markAsReceived(): bool + { + $result = !$this->received; + + $this->received = true; + + return $result; + } +} diff --git a/vendor/php-mqtt/client/src/Repositories/MemoryRepository.php b/vendor/php-mqtt/client/src/Repositories/MemoryRepository.php new file mode 100644 index 0000000..cc409e2 --- /dev/null +++ b/vendor/php-mqtt/client/src/Repositories/MemoryRepository.php @@ -0,0 +1,232 @@ + */ + private array $pendingOutgoingMessages = []; + + /** @var array */ + private array $pendingIncomingMessages = []; + + /** @var array */ + private array $subscriptions = []; + + /** + * {@inheritDoc} + */ + public function reset(): void + { + $this->nextMessageId = 1; + $this->pendingOutgoingMessages = []; + $this->pendingIncomingMessages = []; + $this->subscriptions = []; + } + + /** + * {@inheritDoc} + */ + public function newMessageId(): int + { + if (count($this->pendingOutgoingMessages) >= 65535) { + // This should never happen, as the server receive queue is + // normally smaller than the actual total number of message ids. + // Also, when using MQTT 5.0 the server can specify a smaller + // receive queue size (mosquitto for example has 20 by default), + // so the client has to implement the logic to honor this + // restriction and fallback to the protocol limit. + throw new RepositoryException('No more message identifiers available. The queue is full.'); + } + + while (isset($this->pendingOutgoingMessages[$this->nextMessageId])) { + $this->nextMessageId++; + if ($this->nextMessageId > 65535) { + $this->nextMessageId = 1; + } + } + + return $this->nextMessageId; + } + + /** + * {@inheritDoc} + */ + public function countPendingOutgoingMessages(): int + { + return count($this->pendingOutgoingMessages); + } + + /** + * {@inheritDoc} + */ + public function getPendingOutgoingMessage(int $messageId): ?PendingMessage + { + return $this->pendingOutgoingMessages[$messageId] ?? null; + } + + /** + * {@inheritDoc} + */ + public function getPendingOutgoingMessagesLastSentBefore(\DateTime $dateTime = null): array + { + $result = []; + + foreach ($this->pendingOutgoingMessages as $pendingMessage) { + if ($pendingMessage->getLastSentAt() < $dateTime) { + $result[] = $pendingMessage; + } + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function addPendingOutgoingMessage(PendingMessage $message): void + { + if (isset($this->pendingOutgoingMessages[$message->getMessageId()])) { + throw new PendingMessageAlreadyExistsException($message->getMessageId()); + } + + $this->pendingOutgoingMessages[$message->getMessageId()] = $message; + } + + /** + * {@inheritDoc} + */ + public function markPendingOutgoingPublishedMessageAsReceived(int $messageId): bool + { + if (!isset($this->pendingOutgoingMessages[$messageId]) || + !$this->pendingOutgoingMessages[$messageId] instanceof PublishedMessage) { + throw new PendingMessageNotFoundException($messageId); + } + + return $this->pendingOutgoingMessages[$messageId]->markAsReceived(); + } + + /** + * {@inheritDoc} + */ + public function removePendingOutgoingMessage(int $messageId): bool + { + if (!isset($this->pendingOutgoingMessages[$messageId])) { + return false; + } + + unset($this->pendingOutgoingMessages[$messageId]); + return true; + } + + /** + * {@inheritDoc} + */ + public function countPendingIncomingMessages(): int + { + return count($this->pendingIncomingMessages); + } + + /** + * {@inheritDoc} + */ + public function getPendingIncomingMessage(int $messageId): ?PendingMessage + { + return $this->pendingIncomingMessages[$messageId] ?? null; + } + + /** + * {@inheritDoc} + */ + public function addPendingIncomingMessage(PendingMessage $message): void + { + if (isset($this->pendingIncomingMessages[$message->getMessageId()])) { + throw new PendingMessageAlreadyExistsException($message->getMessageId()); + } + + $this->pendingIncomingMessages[$message->getMessageId()] = $message; + } + + /** + * {@inheritDoc} + */ + public function removePendingIncomingMessage(int $messageId): bool + { + if (!isset($this->pendingIncomingMessages[$messageId])) { + return false; + } + + unset($this->pendingIncomingMessages[$messageId]); + return true; + } + + /** + * {@inheritDoc} + */ + public function countSubscriptions(): int + { + return count($this->subscriptions); + } + + /** + * {@inheritDoc} + */ + public function addSubscription(Subscription $subscription): void + { + // Remove a potentially existing subscription for this topic filter. + $this->removeSubscription($subscription->getTopicFilter()); + + $this->subscriptions[] = $subscription; + } + + /** + * {@inheritDoc} + */ + public function getSubscriptionsMatchingTopic(string $topicName): array + { + $result = []; + + foreach ($this->subscriptions as $subscription) { + if (!$subscription->matchesTopic($topicName)) { + continue; + } + + $result[] = $subscription; + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function removeSubscription(string $topicFilter): bool + { + foreach ($this->subscriptions as $index => $subscription) { + if ($subscription->getTopicFilter() === $topicFilter) { + unset($this->subscriptions[$index]); + return true; + } + } + + return false; + } +} diff --git a/vendor/php-mqtt/client/src/SubscribeRequest.php b/vendor/php-mqtt/client/src/SubscribeRequest.php new file mode 100644 index 0000000..c70e1a4 --- /dev/null +++ b/vendor/php-mqtt/client/src/SubscribeRequest.php @@ -0,0 +1,38 @@ +subscriptions = array_values($subscriptions); + } + + /** + * Returns the subscriptions in this request. + * + * @return Subscription[] + */ + public function getSubscriptions(): array + { + return $this->subscriptions; + } +} diff --git a/vendor/php-mqtt/client/src/Subscription.php b/vendor/php-mqtt/client/src/Subscription.php new file mode 100644 index 0000000..7d2265f --- /dev/null +++ b/vendor/php-mqtt/client/src/Subscription.php @@ -0,0 +1,106 @@ +regexifyTopicFilter(); + } + + /** + * Converts the topic filter into a regular expression. + */ + private function regexifyTopicFilter(): void + { + $topicFilter = $this->topicFilter; + + // If the topic filter is for a shared subscription, we remove the shared subscription prefix as well as the group name + // from the topic filter. To do so, we look for the $share keyword and then try to find the second topic separator to + // calculate the substring containing the actual topic filter. + // Note: shared subscriptions always have the form: $share// + if (str_starts_with($topicFilter, '$share/') && ($separatorIndex = strpos($topicFilter, '/', 7)) !== false) { + $topicFilter = substr($topicFilter, $separatorIndex + 1); + } + + $this->regexifiedTopicFilter = '/^' . str_replace(['$', '/', '+', '#'], ['\$', '\/', '([^\/]*)', '(.*)'], $topicFilter) . '$/'; + } + + /** + * Returns the topic of the subscription. + */ + public function getTopicFilter(): string + { + return $this->topicFilter; + } + + /** + * Matches the given topic name matches to the subscription's topic filter. + */ + public function matchesTopic(string $topicName): bool + { + return (bool) preg_match($this->regexifiedTopicFilter, $topicName); + } + + /** + * Returns an array which contains all matched wildcards of this subscription, taken from the given topic name. + * + * Example: + * Subscription topic filter: foo/+/bar/+/baz/# + * Result for 'foo/1/bar/2/baz': ['1', '2'] + * Result for 'foo/my/bar/subscription/baz/42': ['my', 'subscription', '42'] + * Result for 'foo/my/bar/subscription/baz/hello/world/123': ['my', 'subscription', 'hello', 'world', '123'] + * Result for invalid topic 'some/topic': [] + * + * Note: This method should only be called if {@see matchesTopic} returned true. An empty array will be returned otherwise. + */ + public function getMatchedWildcards(string $topicName): array + { + if (!preg_match($this->regexifiedTopicFilter, $topicName, $matches)) { + return []; + } + + return array_slice($matches, 1); + } + + /** + * Returns the callback for this subscription. + */ + public function getCallback(): ?\Closure + { + return $this->callback; + } + + /** + * Returns the requested quality of service level. + */ + public function getQualityOfServiceLevel(): int + { + return $this->qualityOfService; + } + + /** + * Sets the actual quality of service level. + */ + public function setQualityOfServiceLevel(int $qualityOfService): void + { + $this->qualityOfService = $qualityOfService; + } +} diff --git a/vendor/php-mqtt/client/src/UnsubscribeRequest.php b/vendor/php-mqtt/client/src/UnsubscribeRequest.php new file mode 100644 index 0000000..030e728 --- /dev/null +++ b/vendor/php-mqtt/client/src/UnsubscribeRequest.php @@ -0,0 +1,38 @@ +topicFilters = array_values($topicFilters); + } + + /** + * Returns the topic filters in this request. + * + * @return string[] + */ + public function getTopicFilters(): array + { + return $this->topicFilters; + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/ActionsWithoutActiveConnectionTest.php b/vendor/php-mqtt/client/tests/Feature/ActionsWithoutActiveConnectionTest.php new file mode 100644 index 0000000..da0953f --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/ActionsWithoutActiveConnectionTest.php @@ -0,0 +1,51 @@ +mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected'); + + $this->expectException(ClientNotConnectedToBrokerException::class); + $client->publish('foo/bar', 'baz'); + } + + public function test_throws_exception_when_topic_is_subscribed_without_connecting_to_broker(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected'); + + $this->expectException(ClientNotConnectedToBrokerException::class); + $client->subscribe('foo/bar', fn () => true); + } + + public function test_throws_exception_when_topic_is_unsubscribed_without_connecting_to_broker(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected'); + + $this->expectException(ClientNotConnectedToBrokerException::class); + $client->unsubscribe('foo/bar'); + } + + public function test_throws_exception_when_disconnecting_without_connecting_to_broker_first(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected'); + + $this->expectException(ClientNotConnectedToBrokerException::class); + $client->disconnect(); + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/ClientUtilsTest.php b/vendor/php-mqtt/client/tests/Feature/ClientUtilsTest.php new file mode 100644 index 0000000..00e6866 --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/ClientUtilsTest.php @@ -0,0 +1,93 @@ +mqttBrokerHost, $this->mqttBrokerPort, 'test-byte-count'); + + $client->connect(null, true); + + // Even the connection request and acknowledgement have bytes. + $this->assertGreaterThan(0, $client->getSentBytes()); + $this->assertGreaterThan(0, $client->getReceivedBytes()); + + // We therefore remember the current transfer stats and send some more data. + $sentBytesBeforePublish = $client->getSentBytes(); + $receivedBytesBeforePublish = $client->getReceivedBytes(); + + $client->publish('foo/bar', 'baz-01', MqttClient::QOS_AT_MOST_ONCE); + $client->publish('foo/bar', 'baz-02', MqttClient::QOS_AT_LEAST_ONCE); + $client->publish('foo/bar', 'baz-03', MqttClient::QOS_EXACTLY_ONCE); + + $this->assertGreaterThan($sentBytesBeforePublish, $client->getSentBytes()); + $this->assertSame($receivedBytesBeforePublish, $client->getReceivedBytes()); + + // Also we receive all acknowledgements to update our transfer stats correctly. + $client->loop(true, true); + + $this->assertGreaterThan($receivedBytesBeforePublish, $client->getReceivedBytes()); + + $client->disconnect(); + } + + public function test_is_connected_returns_correct_state(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-is-connected'); + + $client->connect(null, true); + + $this->assertTrue($client->isConnected()); + + $client->disconnect(); + + $this->assertFalse($client->isConnected()); + + $client->connect(null, true); + + $this->assertTrue($client->isConnected()); + + $client->disconnect(); + + $this->assertFalse($client->isConnected()); + } + + public function test_configured_client_id_is_returned_if_client_id_is_passed_to_constructor(): void + { + $clientId = 'test-configured-client-id'; + + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, $clientId); + + $this->assertSame($clientId, $client->getClientId()); + } + + public function test_generated_client_id_is_returned_if_no_client_id_is_passed_to_constructor(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort); + + $this->assertNotNull($client->getClientId()); + $this->assertNotEmpty($client->getClientId()); + } + + public function test_configured_broker_host_and_port_are_returned(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort); + + $this->assertSame($this->mqttBrokerHost, $client->getHost()); + $this->assertSame($this->mqttBrokerPort, $client->getPort()); + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/ConnectWithCustomConnectionSettingsTest.php b/vendor/php-mqtt/client/tests/Feature/ConnectWithCustomConnectionSettingsTest.php new file mode 100644 index 0000000..26f7021 --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/ConnectWithCustomConnectionSettingsTest.php @@ -0,0 +1,83 @@ +mqttBrokerHost, $this->mqttBrokerPortWithAuthentication, 'test-custom-connection-settings', MqttClient::MQTT_3_1); + + $connectionSettings = (new ConnectionSettings) + ->setLastWillTopic('foo/last/will') + ->setLastWillMessage('baz is out!') + ->setLastWillQualityOfService(MqttClient::QOS_AT_MOST_ONCE) + ->setRetainLastWill(true) + ->setConnectTimeout(3) + ->setSocketTimeout(3) + ->setResendTimeout(3) + ->setKeepAliveInterval(30) + ->setUsername($this->mqttBrokerUsername) + ->setPassword($this->mqttBrokerPassword) + ->setUseTls(false) + ->setTlsCertificateAuthorityFile(null) + ->setTlsCertificateAuthorityPath(null) + ->setTlsClientCertificateFile(null) + ->setTlsClientCertificateKeyFile(null) + ->setTlsClientCertificateKeyPassphrase(null) + ->setTlsVerifyPeer(false) + ->setTlsVerifyPeerName(false) + ->setTlsSelfSignedAllowed(true); + + $client->connect($connectionSettings); + + $this->assertTrue($client->isConnected()); + + $client->disconnect(); + } + + public function test_connecting_using_mqtt311_with_custom_connection_settings_works_as_intended(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPortWithAuthentication, 'test-custom-connection-settings', MqttClient::MQTT_3_1_1); + + $connectionSettings = (new ConnectionSettings) + ->setLastWillTopic('foo/last/will') + ->setLastWillMessage('baz is out!') + ->setLastWillQualityOfService(MqttClient::QOS_AT_MOST_ONCE) + ->setRetainLastWill(true) + ->setConnectTimeout(3) + ->setSocketTimeout(3) + ->setResendTimeout(3) + ->setKeepAliveInterval(30) + ->setUsername($this->mqttBrokerUsername) + ->setPassword($this->mqttBrokerPassword) + ->setUseTls(false) + ->setTlsCertificateAuthorityFile(null) + ->setTlsCertificateAuthorityPath(null) + ->setTlsClientCertificateFile(null) + ->setTlsClientCertificateKeyFile(null) + ->setTlsClientCertificateKeyPassphrase(null) + ->setTlsVerifyPeer(false) + ->setTlsVerifyPeerName(false) + ->setTlsSelfSignedAllowed(true); + + $client->connect($connectionSettings); + + $this->assertTrue($client->isConnected()); + + $client->disconnect(); + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/ConnectWithInvalidConfigurationTest.php b/vendor/php-mqtt/client/tests/Feature/ConnectWithInvalidConfigurationTest.php new file mode 100644 index 0000000..eed4ba3 --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/ConnectWithInvalidConfigurationTest.php @@ -0,0 +1,200 @@ +setConnectTimeout($timeout); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + /** + * @dataProvider invalidTimeouts + */ + public function test_socket_timeout_cannot_be_below_1_second(int $timeout): void + { + $connectionSettings = (new ConnectionSettings)->setSocketTimeout($timeout); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + /** + * @dataProvider invalidTimeouts + */ + public function test_resend_timeout_cannot_be_below_1_second(int $timeout): void + { + $connectionSettings = (new ConnectionSettings)->setResendTimeout($timeout); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + public function invalidKeepAliveIntervals(): array + { + return [ + [0], + [-1], + [-100], + [65536], + [100000], + ]; + } + + /** + * @dataProvider invalidKeepAliveIntervals + */ + public function test_keep_alive_interval_cannot_be_value_below_1_or_greater_than_65535(int $keepAliveInterval): void + { + $connectionSettings = (new ConnectionSettings)->setKeepAliveInterval($keepAliveInterval); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + public function invalidUsernames(): array + { + return [ + [''], + [' '], + [' '], + [' '], + ]; + } + + /** + * @dataProvider invalidUsernames + */ + public function test_username_cannot_be_empty_or_whitespace(string $username): void + { + $connectionSettings = (new ConnectionSettings)->setUsername($username); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + public function invalidLastWillTopics(): array + { + return [ + [''], + [' '], + [' '], + [' '], + ]; + } + + /** + * @dataProvider invalidLastWillTopics + */ + public function test_last_will_topic_cannot_be_empty_or_whitespace(string $topic): void + { + $connectionSettings = (new ConnectionSettings)->setLastWillTopic($topic); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + public function invalidLastWillQualityOfService(): array + { + return [ + [-1], + [3], + ]; + } + + /** + * @dataProvider invalidLastWillQualityOfService + */ + public function test_last_will_quality_of_service_cannot_be_outside_the_0_to_2_range(int $qualityOfService): void + { + $connectionSettings = (new ConnectionSettings)->setLastWillQualityOfService($qualityOfService); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + public function test_tls_certificate_authority_file_cannot_be_invalid_file_path(): void + { + $connectionSettings = (new ConnectionSettings)->setTlsCertificateAuthorityFile(__DIR__.'/not_existing_file'); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + public function test_tls_certificate_authority_path_cannot_be_invalid_directory_path(): void + { + $connectionSettings = (new ConnectionSettings)->setTlsCertificateAuthorityPath(__DIR__.'/not_existing_directory'); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + public function test_tls_client_certificate_file_cannot_be_invalid_file_path(): void + { + $connectionSettings = (new ConnectionSettings)->setTlsClientCertificateFile(__DIR__.'/not_existing_file'); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + public function test_tls_client_certificate_key_file_cannot_be_invalid_file_path(): void + { + $connectionSettings = (new ConnectionSettings)->setTlsClientCertificateKeyFile(__DIR__.'/not_existing_file'); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + public function test_tls_client_certificate_file_must_be_set_if_client_certificate_key_file_is_set(): void + { + $connectionSettings = (new ConnectionSettings)->setTlsClientCertificateKeyFile(__DIR__.'/../resources/invalid-test-certificate.key'); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + public function test_tls_client_certificate_key_file_must_be_set_if_client_certificate_key_passphrase_is_set(): void + { + $connectionSettings = (new ConnectionSettings) + ->setTlsClientCertificateFile(__DIR__.'/../resources/invalid-test-certificate.crt') + ->setTlsClientCertificateKeyPassphrase('some'); + + $this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings); + } + + /** + * Performs the actual connection test using the given connection settings. Expects the settings to be invalid. + * + * @throws ConfigurationInvalidException + * @throws ConnectingToBrokerFailedException + * @throws ProtocolNotSupportedException + */ + private function connectAndExpectConfigurationExceptionUsingSettings(ConnectionSettings $connectionSettings): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-invalid-connection-settings'); + + $this->expectException(ConfigurationInvalidException::class); + $client->connect($connectionSettings); + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/ConnectWithInvalidHostAndPortTest.php b/vendor/php-mqtt/client/tests/Feature/ConnectWithInvalidHostAndPortTest.php new file mode 100644 index 0000000..62438f4 --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/ConnectWithInvalidHostAndPortTest.php @@ -0,0 +1,36 @@ +expectException(ConnectingToBrokerFailedException::class); + $this->expectExceptionCode(ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_SOCKET_ERROR); + + try { + $client->connect(null, true); + } catch (ConnectingToBrokerFailedException $e) { + $this->assertGreaterThan(0, $e->getConnectionErrorCode()); + $this->assertNotEmpty($e->getConnectionErrorMessage()); + + throw $e; + } + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/ConnectWithTlsSettingsTest.php b/vendor/php-mqtt/client/tests/Feature/ConnectWithTlsSettingsTest.php new file mode 100644 index 0000000..08b9db4 --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/ConnectWithTlsSettingsTest.php @@ -0,0 +1,136 @@ +skipTlsTests) { + $this->markTestSkipped('TLS tests are disabled.'); + } + } + + public function test_connecting_with_tls_but_without_further_configuration_throws_for_self_signed_certificate(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings'); + + $connectionSettings = (new ConnectionSettings) + ->setUseTls(true); + + $this->expectException(ConnectingToBrokerFailedException::class); + $this->expectExceptionCode(ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_TLS_ERROR); + + $client->connect($connectionSettings, true); + } + + public function test_connecting_with_tls_with_ignored_self_signed_certificate_works_as_intended(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings'); + + $connectionSettings = (new ConnectionSettings) + ->setUseTls(true) + ->setTlsSelfSignedAllowed(true) + ->setTlsVerifyPeer(false) + ->setTlsVerifyPeerName(false); + + $client->connect($connectionSettings, true); + + $this->assertTrue($client->isConnected()); + + $client->disconnect(); + } + + public function test_connecting_with_tls_with_validated_self_signed_certificate_using_cafile__works_as_intended(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings'); + + $connectionSettings = (new ConnectionSettings) + ->setUseTls(true) + ->setTlsSelfSignedAllowed(false) + ->setTlsVerifyPeer(true) + ->setTlsVerifyPeerName(true) + ->setTlsCertificateAuthorityFile($this->tlsCertificateDirectory . '/ca.crt'); + + $client->connect($connectionSettings, true); + + $this->assertTrue($client->isConnected()); + + $client->disconnect(); + } + + public function test_connecting_with_tls_with_validated_self_signed_certificate_using_capath_works_as_intended(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings'); + + $connectionSettings = (new ConnectionSettings) + ->setUseTls(true) + ->setTlsSelfSignedAllowed(false) + ->setTlsVerifyPeer(true) + ->setTlsVerifyPeerName(true) + ->setTlsCertificateAuthorityPath($this->tlsCertificateDirectory); + + $client->connect($connectionSettings, true); + + $this->assertTrue($client->isConnected()); + + $client->disconnect(); + } + + public function test_connecting_with_tls_and_client_certificate_with_validated_self_signed_certificate_works_as_intended(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsWithClientCertificatePort, 'test-tls-settings'); + + $connectionSettings = (new ConnectionSettings) + ->setUseTls(true) + ->setTlsSelfSignedAllowed(false) + ->setTlsVerifyPeer(true) + ->setTlsVerifyPeerName(true) + ->setTlsCertificateAuthorityFile($this->tlsCertificateDirectory . '/ca.crt') + ->setTlsClientCertificateFile($this->tlsCertificateDirectory . '/client.crt') + ->setTlsClientCertificateKeyFile($this->tlsCertificateDirectory . '/client.key'); + + $client->connect($connectionSettings, true); + + $this->assertTrue($client->isConnected()); + + $client->disconnect(); + } + + public function test_connecting_with_tls_and_passphrase_protected_client_certificate_with_validated_self_signed_certificate_works_as_intended(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsWithClientCertificatePort, 'test-tls-settings'); + + $connectionSettings = (new ConnectionSettings) + ->setUseTls(true) + ->setTlsSelfSignedAllowed(false) + ->setTlsVerifyPeer(true) + ->setTlsVerifyPeerName(true) + ->setTlsCertificateAuthorityFile($this->tlsCertificateDirectory . '/ca.crt') + ->setTlsClientCertificateFile($this->tlsCertificateDirectory . '/client2.crt') + ->setTlsClientCertificateKeyFile($this->tlsCertificateDirectory . '/client2.key') + ->setTlsClientCertificateKeyPassphrase('s3cr3t'); + + $client->connect($connectionSettings, true); + + $this->assertTrue($client->isConnected()); + + $client->disconnect(); + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/ConnectedEventHandlerTest.php b/vendor/php-mqtt/client/tests/Feature/ConnectedEventHandlerTest.php new file mode 100644 index 0000000..fcf47b9 --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/ConnectedEventHandlerTest.php @@ -0,0 +1,116 @@ +mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler'); + + $handlerCallCount = 0; + $handler = function () use (&$handlerCallCount) { + $handlerCallCount++; + }; + + $client->registerConnectedEventHandler($handler); + $client->connect(); + + $this->assertSame(1, $handlerCallCount); + + $client->disconnect(); + $client->connect(); + + $this->assertSame(2, $handlerCallCount); + + $client->disconnect(); + $client->connect(); + + $this->assertSame(3, $handlerCallCount); + + $client->disconnect(); + } + + public function test_connected_event_handlers_can_be_unregistered_and_will_not_be_called_anymore(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler'); + + $handlerCallCount = 0; + $handler = function () use (&$handlerCallCount) { + $handlerCallCount++; + }; + + $client->registerConnectedEventHandler($handler); + $client->connect(); + + $this->assertSame(1, $handlerCallCount); + + $client->unregisterConnectedEventHandler($handler); + $client->disconnect(); + $client->connect(); + + $this->assertSame(1, $handlerCallCount); + + $client->registerConnectedEventHandler($handler); + $client->disconnect(); + $client->connect(); + + $this->assertSame(2, $handlerCallCount); + + $client->unregisterConnectedEventHandler($handler); + $client->disconnect(); + $client->connect(); + + $this->assertSame(2, $handlerCallCount); + + $client->disconnect(); + } + + public function test_connected_event_handlers_can_throw_exceptions_which_does_not_affect_other_handlers_or_the_application(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler'); + + $handlerCallCount = 0; + $handler1 = function () use (&$handlerCallCount) { + $handlerCallCount++; + }; + $handler2 = function () { + throw new \Exception('Something went wrong!'); + }; + + $client->registerConnectedEventHandler($handler1); + $client->registerConnectedEventHandler($handler2); + + $client->connect(); + + $this->assertSame(1, $handlerCallCount); + + $client->disconnect(); + } + + public function test_connected_event_handler_is_passed_the_mqtt_client_and_the_auto_reconnect_flag_as_arguments(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler'); + + $client->registerConnectedEventHandler(function ($mqttClient, $isAutoReconnect) { + $this->assertInstanceOf(MqttClient::class, $mqttClient); + $this->assertIsBool($isAutoReconnect); + $this->assertFalse($isAutoReconnect); + }); + + $client->connect(); + $client->disconnect(); + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/LoopEventHandlerTest.php b/vendor/php-mqtt/client/tests/Feature/LoopEventHandlerTest.php new file mode 100644 index 0000000..5b31c63 --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/LoopEventHandlerTest.php @@ -0,0 +1,143 @@ +mqttBrokerHost, $this->mqttBrokerPort, 'test-loop-event-handler'); + + $loopCount = 0; + $previousElapsedTime = 0; + $client->registerLoopEventHandler(function (MqttClient $client, float $elapsedTime) use (&$loopCount, &$previousElapsedTime) { + $this->assertGreaterThanOrEqual($previousElapsedTime, $elapsedTime); + + $previousElapsedTime = $elapsedTime; + + $loopCount++; + + if ($loopCount >= 3) { + $client->interrupt(); + return; + } + }); + + $client->connect(null, true); + + $client->loop(); + + $this->assertSame(3, $loopCount); + + $client->disconnect(); + } + + public function test_loop_event_handler_can_be_unregistered_and_will_not_be_called_anymore(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-loop-event-handler'); + + $loopCount = 0; + $handler = function (MqttClient $client) use (&$loopCount) { + $loopCount++; + + if ($loopCount >= 1) { + $client->interrupt(); + return; + } + }; + + $client->registerLoopEventHandler($handler); + + $client->connect(null, true); + + $client->loop(); + + $this->assertSame(1, $loopCount); + + $client->unregisterLoopEventHandler($handler); + + $client->loop(true, true); + + $this->assertSame(1, $loopCount); + + $client->disconnect(); + } + + public function test_all_loop_event_handlers_can_be_unregistered_and_will_not_be_called_anymore(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-loop-event-handler'); + + $loopCount1 = 0; + $loopCount2 = 0; + $handler1 = function (MqttClient $client) use (&$loopCount1) { + $loopCount1++; + + if ($loopCount1 >= 1) { + $client->interrupt(); + return; + } + }; + $handler2 = function () use (&$loopCount2) { + $loopCount2++; + }; + + $client->registerLoopEventHandler($handler1); + $client->registerLoopEventHandler($handler2); + + $client->connect(null, true); + + $client->loop(); + + $this->assertSame(1, $loopCount1); + $this->assertSame(1, $loopCount2); + + $client->unregisterLoopEventHandler(); + + $client->loop(true, true); + + $this->assertSame(1, $loopCount1); + $this->assertSame(1, $loopCount2); + + $client->disconnect(); + } + + public function test_loop_event_handlers_can_throw_exceptions_which_does_not_affect_other_handlers_or_the_application(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-publish-event-handler'); + + $loopCount = 0; + $handler1 = function () { + throw new \Exception('Something went wrong!'); + }; + $handler2 = function (MqttClient $client) use (&$loopCount) { + $loopCount++; + + if ($loopCount >= 1) { + $client->interrupt(); + return; + } + }; + + $client->registerLoopEventHandler($handler1); + $client->registerLoopEventHandler($handler2); + + $client->connect(null, true); + $client->loop(true); + + $this->assertSame(1, $loopCount); + + $client->disconnect(); + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/MessageReceivedEventHandlerTest.php b/vendor/php-mqtt/client/tests/Feature/MessageReceivedEventHandlerTest.php new file mode 100644 index 0000000..0fac689 --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/MessageReceivedEventHandlerTest.php @@ -0,0 +1,169 @@ +mqttBrokerHost, $this->mqttBrokerPort, 'subscriber'); + $subscriber->connect(null, true); + + $handlerCallCount = 0; + $handler = function (MqttClient $client, string $topic, string $message, int $qualityOfService, bool $retained) use (&$handlerCallCount) { + $handlerCallCount++; + + $this->assertSame('foo/bar/baz', $topic); + $this->assertSame('hello world', $message); + $this->assertSame(0, $qualityOfService); + $this->assertFalse($retained); + + $client->interrupt(); + }; + + $subscriber->registerMessageReceivedEventHandler($handler); + $subscriber->subscribe('foo/bar/baz'); + + // We publish a message from a second client on the same topic. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish('foo/bar/baz', 'hello world', 0, false); + + // Then we loop on the subscriber to (hopefully) receive the published message. + $subscriber->loop(true); + + $this->assertSame(1, $handlerCallCount); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $publisher->disconnect(); + $subscriber->disconnect(); + } + + public function test_message_received_event_handler_can_be_unregistered_and_will_not_be_called_anymore(): void + { + // We connect and subscribe to a topic using the first client. + $subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber'); + $subscriber->connect(null, true); + + $callCount = 0; + $handler = function (MqttClient $client, string $topic, string $message, int $qualityOfService, bool $retained) use (&$handler, &$callCount) { + $callCount++; + + $this->assertSame('foo/bar/baz/01', $topic); + $this->assertSame('hello world', $message); + $this->assertSame(0, $qualityOfService); + $this->assertFalse($retained); + + $client->unregisterMessageReceivedEventHandler($handler); + $client->interrupt(); + }; + + $subscriber->registerMessageReceivedEventHandler($handler); + $subscriber->subscribe('foo/bar/baz/+'); + + // We publish a message from a second client on the same topic. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish('foo/bar/baz/01', 'hello world', 0, false); + $publisher->publish('foo/bar/baz/02', 'hello world', 0, false); + + // Then we loop on the subscriber to (hopefully) receive the published message. + $subscriber->loop(true); + + $this->assertSame(1, $callCount); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $publisher->disconnect(); + $subscriber->disconnect(); + } + + public function test_message_received_event_handlers_can_be_unregistered_and_will_not_be_called_anymore(): void + { + // We connect and subscribe to a topic using the first client. + $subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber'); + $subscriber->connect(null, true); + + $callCount = 0; + $handler = function (MqttClient $client, string $topic, string $message, int $qualityOfService, bool $retained) use (&$callCount) { + $callCount++; + + $this->assertSame('foo/bar/baz/01', $topic); + $this->assertSame('hello world', $message); + $this->assertSame(0, $qualityOfService); + $this->assertFalse($retained); + + $client->unregisterMessageReceivedEventHandler(); + $client->interrupt(); + }; + + $subscriber->registerMessageReceivedEventHandler($handler); + $subscriber->subscribe('foo/bar/baz/+'); + + // We publish a message from a second client on the same topic. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish('foo/bar/baz/01', 'hello world', 0, false); + $publisher->publish('foo/bar/baz/02', 'hello world', 0, false); + + // Then we loop on the subscriber to (hopefully) receive the published message. + $subscriber->loop(true); + + $this->assertSame(1, $callCount); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $publisher->disconnect(); + $subscriber->disconnect(); + } + + public function test_message_received_event_handlers_can_throw_exceptions_which_does_not_affect_other_handlers_or_the_application(): void + { + // We connect and subscribe to a topic using the first client. + $subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber'); + $subscriber->connect(null, true); + + $handlerCallCount = 0; + $handler1 = function () { + throw new \Exception('Something went wrong!'); + }; + $handler2 = function (MqttClient $client) use (&$handlerCallCount) { + $handlerCallCount++; + + $client->interrupt(); + }; + + $subscriber->registerMessageReceivedEventHandler($handler1); + $subscriber->registerMessageReceivedEventHandler($handler2); + $subscriber->subscribe('foo/bar/baz'); + + // We publish a message from a second client on the same topic. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish('foo/bar/baz', 'hello world', 0, false); + + // Then we loop on the subscriber to (hopefully) receive the published message. + $subscriber->loop(true); + + $this->assertSame(1, $handlerCallCount); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $publisher->disconnect(); + $subscriber->disconnect(); + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/PublishEventHandlerTest.php b/vendor/php-mqtt/client/tests/Feature/PublishEventHandlerTest.php new file mode 100644 index 0000000..9b9755c --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/PublishEventHandlerTest.php @@ -0,0 +1,96 @@ +mqttBrokerHost, $this->mqttBrokerPort, 'test-publish-event-handler'); + + $handlerCallCount = 0; + $handler = function () use (&$handlerCallCount) { + $handlerCallCount++; + }; + + $client->registerPublishEventHandler($handler); + + $client->connect(null, true); + $client->publish('foo/bar', 'baz-01'); + $client->publish('foo/bar', 'baz-02'); + $client->publish('foo/bar', 'baz-03'); + + $this->assertSame(3, $handlerCallCount); + + $client->disconnect(); + } + + public function test_publish_event_handlers_can_be_unregistered_and_will_not_be_called_anymore(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-publish-event-handler'); + + $handlerCallCount = 0; + $handler = function () use (&$handlerCallCount) { + $handlerCallCount++; + }; + + $client->registerPublishEventHandler($handler); + + $client->connect(null, true); + $client->publish('foo/bar', 'baz-01'); + + $this->assertSame(1, $handlerCallCount); + + $client->unregisterPublishEventHandler($handler); + $client->publish('foo/bar', 'baz-02'); + + $this->assertSame(1, $handlerCallCount); + + $client->registerPublishEventHandler($handler); + $client->publish('foo/bar', 'baz-03'); + + $this->assertSame(2, $handlerCallCount); + + $client->unregisterPublishEventHandler(); + $client->publish('foo/bar', 'baz-04'); + + $this->assertSame(2, $handlerCallCount); + + $client->disconnect(); + } + + public function test_publish_event_handlers_can_throw_exceptions_which_does_not_affect_other_handlers_or_the_application(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-publish-event-handler'); + + $handlerCallCount = 0; + $handler1 = function () use (&$handlerCallCount) { + $handlerCallCount++; + }; + $handler2 = function () { + throw new \Exception('Something went wrong!'); + }; + + $client->registerPublishEventHandler($handler1); + $client->registerPublishEventHandler($handler2); + + $client->connect(null, true); + $client->publish('foo/bar', 'baz-01'); + + $this->assertSame(1, $handlerCallCount); + + $client->disconnect(); + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/PublishSubscribeTest.php b/vendor/php-mqtt/client/tests/Feature/PublishSubscribeTest.php new file mode 100644 index 0000000..3c76dd3 --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/PublishSubscribeTest.php @@ -0,0 +1,427 @@ +useBlockingSocket($useBlockingSocket); + + $subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber'); + $subscriber->connect($connectionSettings, true); + + $subscriber->subscribe( + $subscriptionTopicFilter, + function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $publishTopic, $publishMessage, $matchedTopicWildcards) { + // By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass. + $this->assertEquals($publishTopic, $topic); + $this->assertEquals($publishMessage, $message); + $this->assertFalse($retained); + $this->assertEquals($matchedTopicWildcards, $wildcards); + + $subscriber->interrupt(); // This allows us to exit the test as soon as possible. + }, + 0 + ); + + // We publish a message from a second client on the same topic. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish($publishTopic, $publishMessage, 0, false); + + // Then we loop on the subscriber to (hopefully) receive the published message. + $subscriber->loop(true); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $publisher->disconnect(); + $subscriber->disconnect(); + } + + /** + * @dataProvider publishSubscribeData + */ + public function test_publishing_and_subscribing_using_quality_of_service_0_with_message_retention_works_as_intended( + bool $useBlockingSocket, + string $subscriptionTopicFilter, + string $publishTopic, + string $publishMessage, + array $matchedTopicWildcards + ): void + { + // We publish a message from the first client, which disconnects before the other client even subscribes. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish($publishTopic, $publishMessage, 0, true); + + $publisher->disconnect(); + + // Because we need to make sure the message reached the broker, we delay the execution for a short period (100ms) intentionally. + // With higher QoS, this is replaced by awaiting delivery of the message. + usleep(100_000); + + // We connect and subscribe to a topic using the second client. + $connectionSettings = (new ConnectionSettings()) + ->useBlockingSocket($useBlockingSocket); + + $subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber'); + $subscriber->connect($connectionSettings, true); + + $subscriber->subscribe( + $subscriptionTopicFilter, + function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $publishTopic, $publishMessage, $matchedTopicWildcards) { + // By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass. + $this->assertEquals($publishTopic, $topic); + $this->assertEquals($publishMessage, $message); + $this->assertTrue($retained); + $this->assertEquals($matchedTopicWildcards, $wildcards); + + $subscriber->interrupt(); // This allows us to exit the test as soon as possible. + }, + 0 + ); + + // Then we loop on the subscriber to (hopefully) receive the published message. + $subscriber->loop(true); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $subscriber->disconnect(); + } + + /** + * @dataProvider publishSubscribeData + */ + public function test_publishing_and_subscribing_using_quality_of_service_1_works_as_intended( + bool $useBlockingSocket, + string $subscriptionTopicFilter, + string $publishTopic, + string $publishMessage, + array $matchedTopicWildcards + ): void + { + // We connect and subscribe to a topic using the first client. + $connectionSettings = (new ConnectionSettings()) + ->useBlockingSocket($useBlockingSocket); + + $subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber'); + $subscriber->connect($connectionSettings, true); + + $subscriber->subscribe( + $subscriptionTopicFilter, + function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $publishTopic, $publishMessage, $matchedTopicWildcards) { + // By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass. + $this->assertEquals($publishTopic, $topic); + $this->assertEquals($publishMessage, $message); + $this->assertFalse($retained); + $this->assertEquals($matchedTopicWildcards, $wildcards); + + $subscriber->interrupt(); // This allows us to exit the test as soon as possible. + }, + 1 + ); + + // We publish a message from a second client on the same topic. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish($publishTopic, $publishMessage, 1, false); + + // Then we loop on the subscriber to (hopefully) receive the published message. + $subscriber->loop(true); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $publisher->disconnect(); + $subscriber->disconnect(); + } + + /** + * @dataProvider publishSubscribeData + */ + public function test_publishing_and_subscribing_using_quality_of_service_1_with_message_retention_works_as_intended( + bool $useBlockingSocket, + string $subscriptionTopicFilter, + string $publishTopic, + string $publishMessage, + array $matchedTopicWildcards + ): void + { + // We publish a message from the first client, which disconnects before the other client even subscribes. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish($publishTopic, $publishMessage, 1, true); + $publisher->loop(true, true); + + $publisher->disconnect(); + + // We connect and subscribe to a topic using the second client. + $connectionSettings = (new ConnectionSettings()) + ->useBlockingSocket($useBlockingSocket); + + $subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber'); + $subscriber->connect($connectionSettings, true); + + $subscriber->subscribe( + $subscriptionTopicFilter, + function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $publishTopic, $publishMessage, $matchedTopicWildcards) { + // By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass. + $this->assertEquals($publishTopic, $topic); + $this->assertEquals($publishMessage, $message); + $this->assertTrue($retained); + $this->assertEquals($matchedTopicWildcards, $wildcards); + + $subscriber->interrupt(); // This allows us to exit the test as soon as possible. + }, + 1 + ); + + // Then we loop on the subscriber to (hopefully) receive the published message. + $subscriber->loop(true); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $subscriber->disconnect(); + } + + /** + * @dataProvider publishSubscribeData + */ + public function test_publishing_and_subscribing_using_quality_of_service_2_works_as_intended( + bool $useBlockingSocket, + string $subscriptionTopicFilter, + string $publishTopic, + string $publishMessage, + array $matchedTopicWildcards + ): void + { + // We connect and subscribe to a topic using the first client. + $connectionSettings = (new ConnectionSettings()) + ->useBlockingSocket($useBlockingSocket); + + $subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber'); + $subscriber->connect($connectionSettings, true); + + $subscription = function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $subscriptionTopicFilter, $publishTopic, $publishMessage, $matchedTopicWildcards) { + // By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass. + $this->assertEquals($publishTopic, $topic); + $this->assertEquals($publishMessage, $message); + $this->assertFalse($retained); + $this->assertEquals($matchedTopicWildcards, $wildcards); + + $subscriber->unsubscribe($subscriptionTopicFilter); + $subscriber->interrupt(); // This allows us to exit the test as soon as possible. + }; + + $subscriber->subscribe($subscriptionTopicFilter, $subscription, 2); + + // We publish a message from a second client on the same topic. The loop is called until all QoS 2 handshakes are done. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish($publishTopic, $publishMessage, 2, false); + $publisher->loop(true, true); + + // Then we loop on the subscriber to (hopefully) receive the published message until the receive handshake is done. + $subscriber->loop(true, true); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $publisher->disconnect(); + $subscriber->disconnect(); + } + + /** + * @dataProvider publishSubscribeData + */ + public function test_publishing_and_subscribing_using_quality_of_service_2_with_message_retention_works_as_intended( + bool $useBlockingSocket, + string $subscriptionTopicFilter, + string $publishTopic, + string $publishMessage, + array $matchedTopicWildcards + ): void + { + // We publish a message from the first client. The loop is called until all QoS 2 handshakes are done. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish($publishTopic, $publishMessage, 2, true); + $publisher->loop(true, true); + + $publisher->disconnect(); + + // We connect and subscribe to a topic using the second client. + $connectionSettings = (new ConnectionSettings()) + ->useBlockingSocket($useBlockingSocket); + + $subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber'); + $subscriber->connect($connectionSettings, true); + + $subscription = function (string $topic, string $message, bool $retained, array $wildcards) use ($subscriber, $subscriptionTopicFilter, $publishTopic, $publishMessage, $matchedTopicWildcards) { + // By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass. + $this->assertEquals($publishTopic, $topic); + $this->assertEquals($publishMessage, $message); + $this->assertTrue($retained); + $this->assertEquals($matchedTopicWildcards, $wildcards); + + $subscriber->unsubscribe($subscriptionTopicFilter); + $subscriber->interrupt(); // This allows us to exit the test as soon as possible. + }; + + $subscriber->subscribe($subscriptionTopicFilter, $subscription, 2); + + // Then we loop on the subscriber to (hopefully) receive the published message until the receive handshake is done. + $subscriber->loop(true, true); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $subscriber->disconnect(); + } + + public function test_unsubscribe_stops_receiving_messages_on_topic(): void + { + // We connect and subscribe to a topic using the first client. + $subscriber = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber'); + $subscriber->connect(null, true); + + $subscribedTopic = 'test/foo/bar/baz'; + $receivedMessageCount = 0; + $subscriber->subscribe( + $subscribedTopic, + function (string $topic, string $message, bool $retained) use ($subscriber, $subscribedTopic, &$receivedMessageCount) { + $receivedMessageCount++; + + // By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass. + $this->assertEquals('test/foo/bar/baz', $topic); + $this->assertEquals('hello world', $message); + $this->assertFalse($retained); + + $subscriber->unsubscribe($subscribedTopic); + }, + 0 + ); + + // We publish a message from a second client on the same topic. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish($subscribedTopic, 'hello world', 0, false); + + // Then we loop on the subscriber to (hopefully) receive the published message. + $subscriber->loop(true, true); + + $this->assertSame(1, $receivedMessageCount); + + $publisher->publish($subscribedTopic, 'hello world #2', 0, false); + $subscriber->loop(true, true); + + // Ensure no second message has been received since we are not subscribed anymore. + $this->assertSame(1, $receivedMessageCount); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $publisher->disconnect(); + $subscriber->disconnect(); + } + + public function test_shared_subscriptions_using_quality_of_service_0_work_as_intended(): void + { + $subscriptionTopicFilter = '$share/test-shared-subscriptions/foo/+'; + $publishTopic = 'foo/bar'; + + // We connect and subscribe to a topic using the first client with a shared subscription. + $subscriber1 = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber1'); + $subscriber1->connect(null, true); + + $subscriber1->subscribe($subscriptionTopicFilter, function (string $topic, string $message, bool $retained) use ($subscriber1, $publishTopic) { + // By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass. + $this->assertEquals($publishTopic, $topic); + $this->assertEquals('hello world #1', $message); + $this->assertFalse($retained); + + $subscriber1->interrupt(); // This allows us to exit the test as soon as possible. + }, 0); + + // We connect and subscribe to a topic using the second client with a shared subscription. + $subscriber2 = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'subscriber2'); + $subscriber2->connect(null, true); + + $subscriber2->subscribe($subscriptionTopicFilter, function (string $topic, string $message, bool $retained) use ($subscriber2, $publishTopic) { + // By asserting something here, we will avoid a no-assertions-in-test warning, making the test pass. + $this->assertEquals($publishTopic, $topic); + $this->assertEquals('hello world #2', $message); + $this->assertFalse($retained); + + $subscriber2->interrupt(); // This allows us to exit the test as soon as possible. + }, 0); + + // We publish a message from a second client on the same topic. + $publisher = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'publisher'); + $publisher->connect(null, true); + + $publisher->publish($publishTopic, 'hello world #1', 0, false); + $publisher->publish($publishTopic, 'hello world #2', 0, false); + + // Then we loop on the subscribers to (hopefully) receive the published messages. + $subscriber1->loop(true); + $subscriber2->loop(true); + + // Finally, we disconnect for a graceful shutdown on the broker side. + $publisher->disconnect(); + $subscriber1->disconnect(); + $subscriber2->disconnect(); + } +} diff --git a/vendor/php-mqtt/client/tests/Feature/SupportedProtocolsTest.php b/vendor/php-mqtt/client/tests/Feature/SupportedProtocolsTest.php new file mode 100644 index 0000000..e506fc3 --- /dev/null +++ b/vendor/php-mqtt/client/tests/Feature/SupportedProtocolsTest.php @@ -0,0 +1,47 @@ +mqttBrokerHost, $this->mqttBrokerPort, 'test-protocol', MqttClient::MQTT_3_1); + + $this->assertInstanceOf(MqttClient::class, $client); + } + + public function test_client_supports_mqtt_3_1_1_protocol(): void + { + $client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-protocol', MqttClient::MQTT_3_1_1); + + $this->assertInstanceOf(MqttClient::class, $client); + } + + public function test_client_does_not_support_mqtt_3_protocol(): void + { + $this->expectException(ProtocolNotSupportedException::class); + + new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-protocol', '3'); + } + + public function test_client_does_not_support_mqtt_5_protocol(): void + { + $this->expectException(ProtocolNotSupportedException::class); + + new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-protocol', '5'); + } +} diff --git a/vendor/php-mqtt/client/tests/TestCase.php b/vendor/php-mqtt/client/tests/TestCase.php new file mode 100644 index 0000000..98741c8 --- /dev/null +++ b/vendor/php-mqtt/client/tests/TestCase.php @@ -0,0 +1,45 @@ +mqttBrokerHost = getenv('MQTT_BROKER_HOST'); + $this->mqttBrokerPort = intval(getenv('MQTT_BROKER_PORT')); + $this->mqttBrokerPortWithAuthentication = intval(getenv('MQTT_BROKER_PORT_WITH_AUTHENTICATION')); + $this->mqttBrokerTlsPort = intval(getenv('MQTT_BROKER_TLS_PORT')); + $this->mqttBrokerTlsWithClientCertificatePort = intval(getenv('MQTT_BROKER_TLS_WITH_CLIENT_CERT_PORT')); + + $this->mqttBrokerUsername = getenv('MQTT_BROKER_USERNAME') ?: null; + $this->mqttBrokerPassword = getenv('MQTT_BROKER_PASSWORD') ?: null; + + $this->skipTlsTests = getenv('SKIP_TLS_TESTS') === 'true'; + $this->tlsCertificateDirectory = rtrim(getenv('TLS_CERT_DIR'), '/'); + } +} diff --git a/vendor/php-mqtt/client/tests/Unit/MessageProcessors/Mqtt311MessageProcessorTest.php b/vendor/php-mqtt/client/tests/Unit/MessageProcessors/Mqtt311MessageProcessorTest.php new file mode 100644 index 0000000..1cd89d2 --- /dev/null +++ b/vendor/php-mqtt/client/tests/Unit/MessageProcessors/Mqtt311MessageProcessorTest.php @@ -0,0 +1,365 @@ +messageProcessor = new Mqtt311MessageProcessor('test-client', new Logger('test.local', 1883, self::CLIENT_ID)); + } + + public function tryFindMessageInBuffer_testDataProvider(): array + { + return [ + // No message and/or no knowledge about the remaining length of the message. + [hex2bin(''), false, null, null], + [hex2bin('20'), false, null, null], + + // Incomplete message with knowledge about the remaining length of the message. + [hex2bin('2002'), false, null, 4], + [hex2bin('200200'), false, null, 4], + + // Buffer contains only one complete message. + [hex2bin('20020000'), true, hex2bin('20020000'), null], + [hex2bin('800a0a03612f6201632f6402'), true, hex2bin('800a0a03612f6201632f6402'), null], + + // Buffer contains more than one complete message. + [hex2bin('2002000044'), true, hex2bin('20020000'), null], + [hex2bin('4002000044'), true, hex2bin('40020000'), null], + [hex2bin('400200004412345678'), true, hex2bin('40020000'), null], + ]; + } + + /** + * @dataProvider tryFindMessageInBuffer_testDataProvider + * + * @param string|null $expectedMessage + * @param int|null $expectedRequiredBytes + */ + public function test_tryFindMessageInBuffer_finds_messages_correctly( + string $buffer, + bool $expectedResult, + ?string $expectedMessage, + ?int $expectedRequiredBytes + ): void + { + $message = null; + $requiredBytes = -1; + + $result = $this->messageProcessor->tryFindMessageInBuffer($buffer, strlen($buffer), $message, $requiredBytes); + + $this->assertEquals($expectedResult, $result); + $this->assertEquals($expectedMessage, $message); + if ($expectedRequiredBytes !== null) { + $this->assertEquals($expectedRequiredBytes, $requiredBytes); + } else { + $this->assertEquals(-1, $requiredBytes); + } + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildConnectMessage_testDataProvider(): array + { + return [ + // Default parameters + [new ConnectionSettings(), false, hex2bin('101700044d5154540400000a000b') . self::CLIENT_ID], + + // Clean Session + [new ConnectionSettings(), true, hex2bin('101700044d5154540402000a000b') . self::CLIENT_ID], + + // Username, Password and Clean Session + [ + (new ConnectionSettings()) + ->setUsername('foo') + ->setPassword('bar'), + true, + hex2bin('102100044d51545404c2000a000b') . self::CLIENT_ID . hex2bin('0003') . 'foo' . hex2bin('0003') . 'bar', + ], + + // Last Will Topic, Last Will Message and Clean Session + [ + (new ConnectionSettings()) + ->setLastWillTopic('test/foo') + ->setLastWillMessage('bar') + ->setLastWillQualityOfService(1), + true, + hex2bin('102600044d515454040e000a000b') . self::CLIENT_ID . hex2bin('0008') . 'test/foo' . hex2bin('0003') . 'bar', + ], + + // Last Will Topic, Last Will Message, Retain Last Will, Username, Password and Clean Session + [ + (new ConnectionSettings()) + ->setLastWillTopic('test/foo') + ->setLastWillMessage('bar') + ->setLastWillQualityOfService(2) + ->setRetainLastWill(true) + ->setUsername('blub') + ->setPassword('blubber'), + true, + hex2bin('103500044d51545404f6000a000b') . self::CLIENT_ID . hex2bin('0008') . 'test/foo' . hex2bin('0003') . 'bar' + . hex2bin('0004') . 'blub' . hex2bin('0007') . 'blubber', + ], + ]; + } + + /** + * @dataProvider buildConnectMessage_testDataProvider + */ + public function test_buildConnectMessage_builds_correct_message( + ConnectionSettings $connectionSettings, + bool $useCleanSession, + string $expectedResult + ): void + { + $result = $this->messageProcessor->buildConnectMessage($connectionSettings, $useCleanSession); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildSubscribeMessage_testDataProvider(): array + { + $longTopic = random_bytes(130); + + return [ + // Simple QoS 0 subscription + [42, [new Subscription('test/foo', 0)], hex2bin('82'.'0d00'.'2a00'.'08') . 'test/foo' . hex2bin('00')], + + // Wildcard QoS 2 subscription with high message id + [43764, [new Subscription('test/foo/bar/baz/#', 2)], hex2bin('82'.'17aa'.'f400'.'12') . 'test/foo/bar/baz/#' . hex2bin('02')], + + // Long QoS 1 subscription with high message id + [62304, [new Subscription($longTopic, 1)], hex2bin('82'.'8701'.'f360'.'0082') . $longTopic . hex2bin('01')], + ]; + } + + /** + * @dataProvider buildSubscribeMessage_testDataProvider + * + * @param Subscription[] $subscriptions + */ + public function test_buildSubscribeMessage_builds_correct_message( + int $messageId, + array $subscriptions, + string $expectedResult + ): void + { + $result = $this->messageProcessor->buildSubscribeMessage($messageId, $subscriptions); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildUnsubscribeMessage_testDataProvider(): array + { + $longTopic = random_bytes(130); + + return [ + // Simple unsubscribe without duplicate + [42, ['test/foo'], false, hex2bin('a2'.'0c00'.'2a00'.'08') . 'test/foo'], + + // Wildcard unsubscribe with high message id as duplicate + [43764, ['test/foo/bar/baz/#'], true, hex2bin('aa'.'16aa'.'f400'.'12') . 'test/foo/bar/baz/#'], + + // Long unsubscribe with high message id as duplicate + [62304, [$longTopic], true, hex2bin('aa'.'8601'.'f360'.'0082') . $longTopic], + ]; + } + + /** + * @dataProvider buildUnsubscribeMessage_testDataProvider + * + * @param string[] $topics + */ + public function test_buildUnsubscribeMessage_builds_correct_message( + int $messageId, + array $topics, + bool $isDuplicate, + string $expectedResult + ): void + { + $result = $this->messageProcessor->buildUnsubscribeMessage($messageId, $topics, $isDuplicate); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildPublishMessage_testDataProvider(): array + { + $longMessage = random_bytes(424242); + + return [ + // Simple QoS 0 publish + ['test/foo', 'hello world', 0, false, 42, false, hex2bin('30'.'17'.'0008') . 'test/foo' . hex2bin('002a') . 'hello world'], + + // Retained duplicate QoS 2 publish with long data and high message id + ['test/foo', $longMessage, 2, true, 4242, true, hex2bin('3d'.'bef219'.'0008') . 'test/foo' . hex2bin('1092') . $longMessage], + ]; + } + + /** + * @dataProvider buildPublishMessage_testDataProvider + */ + public function test_buildPublishMessage_builds_correct_message( + string $topic, + string $message, + int $qualityOfService, + bool $retain, + int $messageId, + bool $isDuplicate, + string $expectedResult + ): void + { + $result = $this->messageProcessor->buildPublishMessage( + $topic, + $message, + $qualityOfService, + $retain, + $messageId, + $isDuplicate + ); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildPublishAcknowledgementMessage_testDataProvider(): array + { + return [ + // Simple acknowledgement using small message id + [42, hex2bin('40'.'02'.'002a')], + + // Simple acknowledgement using large message id + [4242, hex2bin('40'.'02'.'1092')], + ]; + } + + /** + * @dataProvider buildPublishAcknowledgementMessage_testDataProvider + */ + public function test_buildPublishAcknowledgementMessage_builds_correct_message(int $messageId, string $expectedResult): void + { + $result = $this->messageProcessor->buildPublishAcknowledgementMessage($messageId); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildPublishReceivedMessage_testDataProvider(): array + { + return [ + // Simple acknowledgement using small message id + [42, hex2bin('50'.'02'.'002a')], + + // Simple acknowledgement using large message id + [4242, hex2bin('50'.'02'.'1092')], + ]; + } + + /** + * @dataProvider buildPublishReceivedMessage_testDataProvider + */ + public function test_buildPublishReceivedMessage_builds_correct_message(int $messageId, string $expectedResult): void + { + $result = $this->messageProcessor->buildPublishReceivedMessage($messageId); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildPublishCompleteMessage_testDataProvider(): array + { + return [ + // Simple acknowledgement using small message id + [42, hex2bin('70'.'02'.'002a')], + + // Simple acknowledgement using large message id + [4242, hex2bin('70'.'02'.'1092')], + ]; + } + + /** + * @dataProvider buildPublishCompleteMessage_testDataProvider + */ + public function test_buildPublishCompleteMessage_builds_correct_message(int $messageId, string $expectedResult): void + { + $result = $this->messageProcessor->buildPublishCompleteMessage($messageId); + + $this->assertEquals($expectedResult, $result); + } + + public function test_buildPingMessage_builds_correct_message(): void + { + $this->assertEquals(hex2bin('c000'), $this->messageProcessor->buildPingRequestMessage()); + } + + public function test_buildDisconnectMessage_builds_correct_message(): void + { + $this->assertEquals(hex2bin('e000'), $this->messageProcessor->buildDisconnectMessage()); + } +} diff --git a/vendor/php-mqtt/client/tests/Unit/MessageProcessors/Mqtt31MessageProcessorTest.php b/vendor/php-mqtt/client/tests/Unit/MessageProcessors/Mqtt31MessageProcessorTest.php new file mode 100644 index 0000000..ab6dd2e --- /dev/null +++ b/vendor/php-mqtt/client/tests/Unit/MessageProcessors/Mqtt31MessageProcessorTest.php @@ -0,0 +1,365 @@ +messageProcessor = new Mqtt31MessageProcessor('test-client', new Logger('test.local', 1883, self::CLIENT_ID)); + } + + public function tryFindMessageInBuffer_testDataProvider(): array + { + return [ + // No message and/or no knowledge about the remaining length of the message. + [hex2bin(''), false, null, null], + [hex2bin('20'), false, null, null], + + // Incomplete message with knowledge about the remaining length of the message. + [hex2bin('2002'), false, null, 4], + [hex2bin('200200'), false, null, 4], + + // Buffer contains only one complete message. + [hex2bin('20020000'), true, hex2bin('20020000'), null], + [hex2bin('800a0a03612f6201632f6402'), true, hex2bin('800a0a03612f6201632f6402'), null], + + // Buffer contains more than one complete message. + [hex2bin('2002000044'), true, hex2bin('20020000'), null], + [hex2bin('4002000044'), true, hex2bin('40020000'), null], + [hex2bin('400200004412345678'), true, hex2bin('40020000'), null], + ]; + } + + /** + * @dataProvider tryFindMessageInBuffer_testDataProvider + * + * @param string|null $expectedMessage + * @param int|null $expectedRequiredBytes + */ + public function test_tryFindMessageInBuffer_finds_messages_correctly( + string $buffer, + bool $expectedResult, + ?string $expectedMessage, + ?int $expectedRequiredBytes + ): void + { + $message = null; + $requiredBytes = -1; + + $result = $this->messageProcessor->tryFindMessageInBuffer($buffer, strlen($buffer), $message, $requiredBytes); + + $this->assertEquals($expectedResult, $result); + $this->assertEquals($expectedMessage, $message); + if ($expectedRequiredBytes !== null) { + $this->assertEquals($expectedRequiredBytes, $requiredBytes); + } else { + $this->assertEquals(-1, $requiredBytes); + } + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildConnectMessage_testDataProvider(): array + { + return [ + // Default parameters + [new ConnectionSettings(), false, hex2bin('101900064d51497364700300000a000b') . self::CLIENT_ID], + + // Clean Session + [new ConnectionSettings(), true, hex2bin('101900064d51497364700302000a000b') . self::CLIENT_ID], + + // Username, Password and Clean Session + [ + (new ConnectionSettings()) + ->setUsername('foo') + ->setPassword('bar'), + true, + hex2bin('102300064d514973647003c2000a000b') . self::CLIENT_ID . hex2bin('0003') . 'foo' . hex2bin('0003') . 'bar', + ], + + // Last Will Topic, Last Will Message and Clean Session + [ + (new ConnectionSettings()) + ->setLastWillTopic('test/foo') + ->setLastWillMessage('bar') + ->setLastWillQualityOfService(1), + true, + hex2bin('102800064d5149736470030e000a000b') . self::CLIENT_ID . hex2bin('0008') . 'test/foo' . hex2bin('0003') . 'bar', + ], + + // Last Will Topic, Last Will Message, Retain Last Will, Username, Password and Clean Session + [ + (new ConnectionSettings()) + ->setLastWillTopic('test/foo') + ->setLastWillMessage('bar') + ->setLastWillQualityOfService(2) + ->setRetainLastWill(true) + ->setUsername('blub') + ->setPassword('blubber'), + true, + hex2bin('103700064d514973647003f6000a000b') . self::CLIENT_ID . hex2bin('0008') . 'test/foo' . hex2bin('0003') . 'bar' + . hex2bin('0004') . 'blub' . hex2bin('0007') . 'blubber', + ], + ]; + } + + /** + * @dataProvider buildConnectMessage_testDataProvider + */ + public function test_buildConnectMessage_builds_correct_message( + ConnectionSettings $connectionSettings, + bool $useCleanSession, + string $expectedResult + ): void + { + $result = $this->messageProcessor->buildConnectMessage($connectionSettings, $useCleanSession); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildSubscribeMessage_testDataProvider(): array + { + $longTopic = random_bytes(130); + + return [ + // Simple QoS 0 subscription + [42, [new Subscription('test/foo', 0)], hex2bin('82'.'0d00'.'2a00'.'08') . 'test/foo' . hex2bin('00')], + + // Wildcard QoS 2 subscription with high message id + [43764, [new Subscription('test/foo/bar/baz/#', 2)], hex2bin('82'.'17aa'.'f400'.'12') . 'test/foo/bar/baz/#' . hex2bin('02')], + + // Long QoS 1 subscription with high message id + [62304, [new Subscription($longTopic, 1)], hex2bin('82'.'8701'.'f360'.'0082') . $longTopic . hex2bin('01')], + ]; + } + + /** + * @dataProvider buildSubscribeMessage_testDataProvider + * + * @param Subscription[] $subscriptions + */ + public function test_buildSubscribeMessage_builds_correct_message( + int $messageId, + array $subscriptions, + string $expectedResult + ): void + { + $result = $this->messageProcessor->buildSubscribeMessage($messageId, $subscriptions); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildUnsubscribeMessage_testDataProvider(): array + { + $longTopic = random_bytes(130); + + return [ + // Simple unsubscribe without duplicate + [42, ['test/foo'], false, hex2bin('a2'.'0c00'.'2a00'.'08') . 'test/foo'], + + // Wildcard unsubscribe with high message id as duplicate + [43764, ['test/foo/bar/baz/#'], true, hex2bin('aa'.'16aa'.'f400'.'12') . 'test/foo/bar/baz/#'], + + // Long unsubscribe with high message id as duplicate + [62304, [$longTopic], true, hex2bin('aa'.'8601'.'f360'.'0082') . $longTopic], + ]; + } + + /** + * @dataProvider buildUnsubscribeMessage_testDataProvider + * + * @param string[] $topics + */ + public function test_buildUnsubscribeMessage_builds_correct_message( + int $messageId, + array $topics, + bool $isDuplicate, + string $expectedResult + ): void + { + $result = $this->messageProcessor->buildUnsubscribeMessage($messageId, $topics, $isDuplicate); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildPublishMessage_testDataProvider(): array + { + $longMessage = random_bytes(424242); + + return [ + // Simple QoS 0 publish + ['test/foo', 'hello world', 0, false, 42, false, hex2bin('30'.'17'.'0008') . 'test/foo' . hex2bin('002a') . 'hello world'], + + // Retained duplicate QoS 2 publish with long data and high message id + ['test/foo', $longMessage, 2, true, 4242, true, hex2bin('3d'.'bef219'.'0008') . 'test/foo' . hex2bin('1092') . $longMessage], + ]; + } + + /** + * @dataProvider buildPublishMessage_testDataProvider + */ + public function test_buildPublishMessage_builds_correct_message( + string $topic, + string $message, + int $qualityOfService, + bool $retain, + int $messageId, + bool $isDuplicate, + string $expectedResult + ): void + { + $result = $this->messageProcessor->buildPublishMessage( + $topic, + $message, + $qualityOfService, + $retain, + $messageId, + $isDuplicate + ); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildPublishAcknowledgementMessage_testDataProvider(): array + { + return [ + // Simple acknowledgement using small message id + [42, hex2bin('40'.'02'.'002a')], + + // Simple acknowledgement using large message id + [4242, hex2bin('40'.'02'.'1092')], + ]; + } + + /** + * @dataProvider buildPublishAcknowledgementMessage_testDataProvider + */ + public function test_buildPublishAcknowledgementMessage_builds_correct_message(int $messageId, string $expectedResult): void + { + $result = $this->messageProcessor->buildPublishAcknowledgementMessage($messageId); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildPublishReceivedMessage_testDataProvider(): array + { + return [ + // Simple acknowledgement using small message id + [42, hex2bin('50'.'02'.'002a')], + + // Simple acknowledgement using large message id + [4242, hex2bin('50'.'02'.'1092')], + ]; + } + + /** + * @dataProvider buildPublishReceivedMessage_testDataProvider + */ + public function test_buildPublishReceivedMessage_builds_correct_message(int $messageId, string $expectedResult): void + { + $result = $this->messageProcessor->buildPublishReceivedMessage($messageId); + + $this->assertEquals($expectedResult, $result); + } + + /** + * Message format: + * + * + * + * @return array[] + * @throws \Exception + */ + public function buildPublishCompleteMessage_testDataProvider(): array + { + return [ + // Simple acknowledgement using small message id + [42, hex2bin('70'.'02'.'002a')], + + // Simple acknowledgement using large message id + [4242, hex2bin('70'.'02'.'1092')], + ]; + } + + /** + * @dataProvider buildPublishCompleteMessage_testDataProvider + */ + public function test_buildPublishCompleteMessage_builds_correct_message(int $messageId, string $expectedResult): void + { + $result = $this->messageProcessor->buildPublishCompleteMessage($messageId); + + $this->assertEquals($expectedResult, $result); + } + + public function test_buildPingMessage_builds_correct_message(): void + { + $this->assertEquals(hex2bin('c000'), $this->messageProcessor->buildPingRequestMessage()); + } + + public function test_buildDisconnectMessage_builds_correct_message(): void + { + $this->assertEquals(hex2bin('e000'), $this->messageProcessor->buildDisconnectMessage()); + } +} diff --git a/vendor/php-mqtt/client/tests/resources/invalid-test-ca.crt b/vendor/php-mqtt/client/tests/resources/invalid-test-ca.crt new file mode 100644 index 0000000..e69de29 diff --git a/vendor/php-mqtt/client/tests/resources/invalid-test-certificate.crt b/vendor/php-mqtt/client/tests/resources/invalid-test-certificate.crt new file mode 100644 index 0000000..e69de29 diff --git a/vendor/php-mqtt/client/tests/resources/invalid-test-certificate.key b/vendor/php-mqtt/client/tests/resources/invalid-test-certificate.key new file mode 100644 index 0000000..e69de29 diff --git a/vendor/services.php b/vendor/services.php index ec1e598..405edf8 100644 --- a/vendor/services.php +++ b/vendor/services.php @@ -1,5 +1,5 @@ 'think\\app\\Service',