mirror of
https://github.com/actix/actix-extras.git
synced 2025-09-02 13:06:38 +02:00
add MQTT and AMQP packages
This commit is contained in:
24
actix-mqtt/CHANGES.md
Executable file
24
actix-mqtt/CHANGES.md
Executable file
@@ -0,0 +1,24 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 0.2.3 - 2020-03-10
|
||||
* Add server handshake timeout
|
||||
|
||||
|
||||
## 0.2.2 - 2020-02-04
|
||||
* Fix server keep-alive impl
|
||||
|
||||
|
||||
## 0.2.1 - 2019-12-25
|
||||
* Allow to specify multi-pattern for topics
|
||||
|
||||
|
||||
## 0.2.0 - 2019-12-11
|
||||
* Migrate to `std::future`
|
||||
* Support publish with QoS 1
|
||||
|
||||
|
||||
## 0.1.0 - 2019-09-25
|
||||
* Initial release
|
38
actix-mqtt/Cargo.toml
Executable file
38
actix-mqtt/Cargo.toml
Executable file
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "actix-mqtt"
|
||||
version = "0.2.3"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "MQTT v3.1.1 Client/Server framework"
|
||||
documentation = "https://docs.rs/actix-mqtt"
|
||||
repository = "https://github.com/actix/actix-extras.git"
|
||||
categories = ["network-programming"]
|
||||
keywords = ["MQTT", "IoT", "messaging"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
mqtt-codec = "0.3.0"
|
||||
actix-codec = "0.2.0"
|
||||
actix-service = "1.0.1"
|
||||
actix-utils = "1.0.4"
|
||||
actix-router = "0.2.2"
|
||||
actix-ioframe = "0.4.1"
|
||||
actix-rt = "1.0.0"
|
||||
|
||||
derive_more = "0.99.2"
|
||||
bytes = "0.5.3"
|
||||
either = "1.5.2"
|
||||
futures = "0.3.1"
|
||||
pin-project = "0.4.6"
|
||||
log = "0.4"
|
||||
bytestring = "0.1.2"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.6"
|
||||
actix-connect = "1.0.1"
|
||||
actix-server = "1.0.0"
|
||||
actix-testing = "1.0.0"
|
||||
actix-rt = "1.0.0"
|
201
actix-mqtt/LICENSE-APACHE
Executable file
201
actix-mqtt/LICENSE-APACHE
Executable file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2019-NOW Nikolay Kim
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
25
actix-mqtt/LICENSE-MIT
Executable file
25
actix-mqtt/LICENSE-MIT
Executable file
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2019 Nikolay Kim
|
||||
|
||||
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.
|
1
actix-mqtt/README.md
Executable file
1
actix-mqtt/README.md
Executable file
@@ -0,0 +1 @@
|
||||
# MQTT 3.1.1 Client/Server framework [](https://travis-ci.org/actix/actix-mqtt) [](https://codecov.io/gh/actix/actix-mqtt) [](https://crates.io/crates/actix-mqtt) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
17
actix-mqtt/codec/CHANGES.md
Executable file
17
actix-mqtt/codec/CHANGES.md
Executable file
@@ -0,0 +1,17 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2020-xx-xx
|
||||
|
||||
|
||||
## 0.3.0 - 2019-12-11
|
||||
* Use `bytestring` instead of string
|
||||
* Upgrade actix-codec to 0.2.0
|
||||
|
||||
|
||||
## 0.2.0 - 2019-09-09
|
||||
* Remove `Packet::Empty`
|
||||
* Add `max frame size` config
|
||||
|
||||
|
||||
## 0.1.0 - 2019-06-18
|
||||
* Initial release
|
21
actix-mqtt/codec/Cargo.toml
Executable file
21
actix-mqtt/codec/Cargo.toml
Executable file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "mqtt-codec"
|
||||
version = "0.3.0"
|
||||
authors = [
|
||||
"Max Gortman <mgortman@microsoft.com>",
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Flier Lu <flier.lu@gmail.com>",
|
||||
]
|
||||
description = "MQTT v3.1.1 Codec"
|
||||
documentation = "https://docs.rs/mqtt-codec"
|
||||
repository = "https://github.com/actix/actix-mqtt.git"
|
||||
readme = "README.md"
|
||||
keywords = ["MQTT", "IoT", "messaging"]
|
||||
license = "MIT/Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.0"
|
||||
bytes = "0.5.2"
|
||||
bytestring = "0.1.0"
|
||||
actix-codec = "0.2.0"
|
201
actix-mqtt/codec/LICENSE-APACHE
Executable file
201
actix-mqtt/codec/LICENSE-APACHE
Executable file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2019-NOW Nikolay Kim
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
25
actix-mqtt/codec/LICENSE-MIT
Executable file
25
actix-mqtt/codec/LICENSE-MIT
Executable file
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2019 Nikolay Kim
|
||||
|
||||
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.
|
3
actix-mqtt/codec/README.md
Executable file
3
actix-mqtt/codec/README.md
Executable file
@@ -0,0 +1,3 @@
|
||||
# MQTT v3.1 Codec
|
||||
|
||||
MQTT v3.1 Codec implementation
|
616
actix-mqtt/codec/src/codec/decode.rs
Executable file
616
actix-mqtt/codec/src/codec/decode.rs
Executable file
@@ -0,0 +1,616 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::num::{NonZeroU16, NonZeroU32};
|
||||
|
||||
use bytes::{Buf, Bytes};
|
||||
use bytestring::ByteString;
|
||||
|
||||
use crate::error::ParseError;
|
||||
use crate::packet::*;
|
||||
use crate::proto::*;
|
||||
|
||||
use super::{ConnectAckFlags, ConnectFlags, FixedHeader, WILL_QOS_SHIFT};
|
||||
|
||||
pub(crate) fn read_packet(mut src: Bytes, header: FixedHeader) -> Result<Packet, ParseError> {
|
||||
match header.packet_type {
|
||||
CONNECT => decode_connect_packet(&mut src),
|
||||
CONNACK => decode_connect_ack_packet(&mut src),
|
||||
PUBLISH => decode_publish_packet(&mut src, header),
|
||||
PUBACK => Ok(Packet::PublishAck {
|
||||
packet_id: NonZeroU16::parse(&mut src)?,
|
||||
}),
|
||||
PUBREC => Ok(Packet::PublishReceived {
|
||||
packet_id: NonZeroU16::parse(&mut src)?,
|
||||
}),
|
||||
PUBREL => Ok(Packet::PublishRelease {
|
||||
packet_id: NonZeroU16::parse(&mut src)?,
|
||||
}),
|
||||
PUBCOMP => Ok(Packet::PublishComplete {
|
||||
packet_id: NonZeroU16::parse(&mut src)?,
|
||||
}),
|
||||
SUBSCRIBE => decode_subscribe_packet(&mut src),
|
||||
SUBACK => decode_subscribe_ack_packet(&mut src),
|
||||
UNSUBSCRIBE => decode_unsubscribe_packet(&mut src),
|
||||
UNSUBACK => Ok(Packet::UnsubscribeAck {
|
||||
packet_id: NonZeroU16::parse(&mut src)?,
|
||||
}),
|
||||
PINGREQ => Ok(Packet::PingRequest),
|
||||
PINGRESP => Ok(Packet::PingResponse),
|
||||
DISCONNECT => Ok(Packet::Disconnect),
|
||||
_ => Err(ParseError::UnsupportedPacketType),
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! check_flag {
|
||||
($flags:expr, $flag:expr) => {
|
||||
($flags & $flag.bits()) == $flag.bits()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! ensure {
|
||||
($cond:expr, $e:expr) => {
|
||||
if !($cond) {
|
||||
return Err($e);
|
||||
}
|
||||
};
|
||||
($cond:expr, $fmt:expr, $($arg:tt)+) => {
|
||||
if !($cond) {
|
||||
return Err($fmt, $($arg)+);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn decode_variable_length(src: &[u8]) -> Result<Option<(usize, usize)>, ParseError> {
|
||||
if let Some((len, consumed, more)) = src
|
||||
.iter()
|
||||
.enumerate()
|
||||
.scan((0, true), |state, (idx, x)| {
|
||||
if !state.1 || idx > 3 {
|
||||
return None;
|
||||
}
|
||||
state.0 += ((x & 0x7F) as usize) << (idx * 7);
|
||||
state.1 = x & 0x80 != 0;
|
||||
Some((state.0, idx + 1, state.1))
|
||||
})
|
||||
.last()
|
||||
{
|
||||
ensure!(!more || consumed < 4, ParseError::InvalidLength);
|
||||
return Ok(Some((len, consumed)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn decode_connect_packet(src: &mut Bytes) -> Result<Packet, ParseError> {
|
||||
ensure!(src.remaining() >= 10, ParseError::InvalidLength);
|
||||
let len = src.get_u16();
|
||||
|
||||
ensure!(
|
||||
len == 4 && &src.bytes()[0..4] == b"MQTT",
|
||||
ParseError::InvalidProtocol
|
||||
);
|
||||
src.advance(4);
|
||||
|
||||
let level = src.get_u8();
|
||||
ensure!(
|
||||
level == DEFAULT_MQTT_LEVEL,
|
||||
ParseError::UnsupportedProtocolLevel
|
||||
);
|
||||
|
||||
let flags = src.get_u8();
|
||||
ensure!((flags & 0x01) == 0, ParseError::ConnectReservedFlagSet);
|
||||
|
||||
let keep_alive = src.get_u16();
|
||||
let client_id = decode_utf8_str(src)?;
|
||||
|
||||
ensure!(
|
||||
!client_id.is_empty() || check_flag!(flags, ConnectFlags::CLEAN_SESSION),
|
||||
ParseError::InvalidClientId
|
||||
);
|
||||
|
||||
let topic = if check_flag!(flags, ConnectFlags::WILL) {
|
||||
Some(decode_utf8_str(src)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let message = if check_flag!(flags, ConnectFlags::WILL) {
|
||||
Some(decode_length_bytes(src)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let username = if check_flag!(flags, ConnectFlags::USERNAME) {
|
||||
Some(decode_utf8_str(src)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let password = if check_flag!(flags, ConnectFlags::PASSWORD) {
|
||||
Some(decode_length_bytes(src)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let last_will = if let Some(topic) = topic {
|
||||
Some(LastWill {
|
||||
qos: QoS::from((flags & ConnectFlags::WILL_QOS.bits()) >> WILL_QOS_SHIFT),
|
||||
retain: check_flag!(flags, ConnectFlags::WILL_RETAIN),
|
||||
topic,
|
||||
message: message.unwrap(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Packet::Connect(Connect {
|
||||
protocol: Protocol::MQTT(level),
|
||||
clean_session: check_flag!(flags, ConnectFlags::CLEAN_SESSION),
|
||||
keep_alive,
|
||||
client_id,
|
||||
last_will,
|
||||
username,
|
||||
password,
|
||||
}))
|
||||
}
|
||||
|
||||
fn decode_connect_ack_packet(src: &mut Bytes) -> Result<Packet, ParseError> {
|
||||
ensure!(src.remaining() >= 2, ParseError::InvalidLength);
|
||||
let flags = src.get_u8();
|
||||
ensure!(
|
||||
(flags & 0b1111_1110) == 0,
|
||||
ParseError::ConnAckReservedFlagSet
|
||||
);
|
||||
|
||||
let return_code = src.get_u8();
|
||||
Ok(Packet::ConnectAck {
|
||||
session_present: check_flag!(flags, ConnectAckFlags::SESSION_PRESENT),
|
||||
return_code: ConnectCode::from(return_code),
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_publish_packet(src: &mut Bytes, header: FixedHeader) -> Result<Packet, ParseError> {
|
||||
let topic = decode_utf8_str(src)?;
|
||||
let qos = QoS::from((header.packet_flags & 0b0110) >> 1);
|
||||
let packet_id = if qos == QoS::AtMostOnce {
|
||||
None
|
||||
} else {
|
||||
Some(NonZeroU16::parse(src)?)
|
||||
};
|
||||
|
||||
let len = src.remaining();
|
||||
let payload = src.split_to(len);
|
||||
|
||||
Ok(Packet::Publish(Publish {
|
||||
dup: (header.packet_flags & 0b1000) == 0b1000,
|
||||
qos,
|
||||
retain: (header.packet_flags & 0b0001) == 0b0001,
|
||||
topic,
|
||||
packet_id,
|
||||
payload,
|
||||
}))
|
||||
}
|
||||
|
||||
fn decode_subscribe_packet(src: &mut Bytes) -> Result<Packet, ParseError> {
|
||||
let packet_id = NonZeroU16::parse(src)?;
|
||||
let mut topic_filters = Vec::new();
|
||||
while src.remaining() > 0 {
|
||||
let topic = decode_utf8_str(src)?;
|
||||
ensure!(src.remaining() >= 1, ParseError::InvalidLength);
|
||||
let qos = QoS::from(src.get_u8() & 0x03);
|
||||
topic_filters.push((topic, qos));
|
||||
}
|
||||
|
||||
Ok(Packet::Subscribe {
|
||||
packet_id,
|
||||
topic_filters,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_subscribe_ack_packet(src: &mut Bytes) -> Result<Packet, ParseError> {
|
||||
let packet_id = NonZeroU16::parse(src)?;
|
||||
let status = src
|
||||
.bytes()
|
||||
.iter()
|
||||
.map(|code| {
|
||||
if *code == 0x80 {
|
||||
SubscribeReturnCode::Failure
|
||||
} else {
|
||||
SubscribeReturnCode::Success(QoS::from(code & 0x03))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(Packet::SubscribeAck { packet_id, status })
|
||||
}
|
||||
|
||||
fn decode_unsubscribe_packet(src: &mut Bytes) -> Result<Packet, ParseError> {
|
||||
let packet_id = NonZeroU16::parse(src)?;
|
||||
let mut topic_filters = Vec::new();
|
||||
while src.remaining() > 0 {
|
||||
topic_filters.push(decode_utf8_str(src)?);
|
||||
}
|
||||
Ok(Packet::Unsubscribe {
|
||||
packet_id,
|
||||
topic_filters,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_length_bytes(src: &mut Bytes) -> Result<Bytes, ParseError> {
|
||||
let len: u16 = NonZeroU16::parse(src)?.into();
|
||||
ensure!(src.remaining() >= len as usize, ParseError::InvalidLength);
|
||||
Ok(src.split_to(len as usize))
|
||||
}
|
||||
|
||||
fn decode_utf8_str(src: &mut Bytes) -> Result<ByteString, ParseError> {
|
||||
Ok(ByteString::try_from(decode_length_bytes(src)?)?)
|
||||
}
|
||||
|
||||
pub(crate) trait ByteBuf: Buf {
|
||||
fn inner_mut(&mut self) -> &mut Bytes;
|
||||
}
|
||||
|
||||
impl ByteBuf for Bytes {
|
||||
fn inner_mut(&mut self) -> &mut Bytes {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ByteBuf for bytes::buf::ext::Take<&mut Bytes> {
|
||||
fn inner_mut(&mut self) -> &mut Bytes {
|
||||
self.get_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait Parse: Sized {
|
||||
fn parse<B: ByteBuf>(src: &mut B) -> Result<Self, ParseError>;
|
||||
}
|
||||
|
||||
impl Parse for bool {
|
||||
fn parse<B: ByteBuf>(src: &mut B) -> Result<Self, ParseError> {
|
||||
ensure!(src.has_remaining(), ParseError::InvalidLength); // expected more data within the field
|
||||
let v = src.get_u8();
|
||||
ensure!(v <= 0x1, ParseError::MalformedPacket); // value is invalid
|
||||
Ok(v == 0x1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for u16 {
|
||||
fn parse<B: ByteBuf>(src: &mut B) -> Result<Self, ParseError> {
|
||||
ensure!(src.remaining() >= 2, ParseError::InvalidLength);
|
||||
Ok(src.get_u16())
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for u32 {
|
||||
fn parse<B: ByteBuf>(src: &mut B) -> Result<Self, ParseError> {
|
||||
ensure!(src.remaining() >= 4, ParseError::InvalidLength); // expected more data within the field
|
||||
let val = src.get_u32();
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for NonZeroU32 {
|
||||
fn parse<B: ByteBuf>(src: &mut B) -> Result<Self, ParseError> {
|
||||
let val = NonZeroU32::new(u32::parse(src)?).ok_or(ParseError::MalformedPacket)?;
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for NonZeroU16 {
|
||||
fn parse<B: ByteBuf>(src: &mut B) -> Result<Self, ParseError> {
|
||||
Ok(NonZeroU16::new(u16::parse(src)?).ok_or(ParseError::MalformedPacket)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Bytes {
|
||||
fn parse<B: ByteBuf>(src: &mut B) -> Result<Self, ParseError> {
|
||||
let len = u16::parse(src)? as usize;
|
||||
ensure!(src.remaining() >= len, ParseError::InvalidLength);
|
||||
Ok(src.inner_mut().split_to(len))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type ByteStr = ByteString;
|
||||
|
||||
impl Parse for ByteStr {
|
||||
fn parse<B: ByteBuf>(src: &mut B) -> Result<Self, ParseError> {
|
||||
let bytes = Bytes::parse(src)?;
|
||||
Ok(ByteString::try_from(bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for (ByteStr, ByteStr) {
|
||||
fn parse<B: ByteBuf>(src: &mut B) -> Result<Self, ParseError> {
|
||||
let key = ByteStr::parse(src)?;
|
||||
let val = ByteStr::parse(src)?;
|
||||
Ok((key, val))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
macro_rules! assert_decode_packet (
|
||||
($bytes:expr, $res:expr) => {{
|
||||
let fixed = $bytes.as_ref()[0];
|
||||
let (_len, consumned) = decode_variable_length(&$bytes[1..]).unwrap().unwrap();
|
||||
let hdr = FixedHeader {
|
||||
packet_type: fixed >> 4,
|
||||
packet_flags: fixed & 0xF,
|
||||
remaining_length: $bytes.len() - consumned - 1,
|
||||
};
|
||||
let cur = Bytes::from_static(&$bytes[consumned + 1..]);
|
||||
assert_eq!(read_packet(cur, hdr), Ok($res));
|
||||
}};
|
||||
);
|
||||
|
||||
fn packet_id(v: u16) -> NonZeroU16 {
|
||||
NonZeroU16::new(v).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_variable_length() {
|
||||
macro_rules! assert_variable_length (
|
||||
($bytes:expr, $res:expr) => {{
|
||||
assert_eq!(decode_variable_length($bytes), Ok(Some($res)));
|
||||
}};
|
||||
|
||||
($bytes:expr, $res:expr, $rest:expr) => {{
|
||||
assert_eq!(decode_variable_length($bytes), Ok(Some($res)));
|
||||
}};
|
||||
);
|
||||
|
||||
assert_variable_length!(b"\x7f\x7f", (127, 1), b"\x7f");
|
||||
|
||||
//assert_eq!(decode_variable_length(b"\xff\xff\xff"), Ok(None));
|
||||
assert_eq!(
|
||||
decode_variable_length(b"\xff\xff\xff\xff\xff\xff"),
|
||||
Err(ParseError::InvalidLength)
|
||||
);
|
||||
|
||||
assert_variable_length!(b"\x00", (0, 1));
|
||||
assert_variable_length!(b"\x7f", (127, 1));
|
||||
assert_variable_length!(b"\x80\x01", (128, 2));
|
||||
assert_variable_length!(b"\xff\x7f", (16383, 2));
|
||||
assert_variable_length!(b"\x80\x80\x01", (16384, 3));
|
||||
assert_variable_length!(b"\xff\xff\x7f", (2097151, 3));
|
||||
assert_variable_length!(b"\x80\x80\x80\x01", (2097152, 4));
|
||||
assert_variable_length!(b"\xff\xff\xff\x7f", (268435455, 4));
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_decode_header() {
|
||||
// assert_eq!(
|
||||
// decode_header(b"\x20\x7f"),
|
||||
// Done(
|
||||
// &b""[..],
|
||||
// FixedHeader {
|
||||
// packet_type: CONNACK,
|
||||
// packet_flags: 0,
|
||||
// remaining_length: 127,
|
||||
// }
|
||||
// )
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// decode_header(b"\x3C\x82\x7f"),
|
||||
// Done(
|
||||
// &b""[..],
|
||||
// FixedHeader {
|
||||
// packet_type: PUBLISH,
|
||||
// packet_flags: 0x0C,
|
||||
// remaining_length: 16258,
|
||||
// }
|
||||
// )
|
||||
// );
|
||||
|
||||
// assert_eq!(decode_header(b"\x20"), Incomplete(Needed::Unknown));
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_decode_connect_packets() {
|
||||
assert_eq!(
|
||||
decode_connect_packet(&mut Bytes::from_static(
|
||||
b"\x00\x04MQTT\x04\xC0\x00\x3C\x00\x0512345\x00\x04user\x00\x04pass"
|
||||
)),
|
||||
Ok(Packet::Connect(Connect {
|
||||
protocol: Protocol::MQTT(4),
|
||||
clean_session: false,
|
||||
keep_alive: 60,
|
||||
client_id: ByteString::try_from(Bytes::from_static(b"12345")).unwrap(),
|
||||
last_will: None,
|
||||
username: Some(ByteString::try_from(Bytes::from_static(b"user")).unwrap()),
|
||||
password: Some(Bytes::from(&b"pass"[..])),
|
||||
}))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
decode_connect_packet(&mut Bytes::from_static(
|
||||
b"\x00\x04MQTT\x04\x14\x00\x3C\x00\x0512345\x00\x05topic\x00\x07message"
|
||||
)),
|
||||
Ok(Packet::Connect(Connect {
|
||||
protocol: Protocol::MQTT(4),
|
||||
clean_session: false,
|
||||
keep_alive: 60,
|
||||
client_id: ByteString::try_from(Bytes::from_static(b"12345")).unwrap(),
|
||||
last_will: Some(LastWill {
|
||||
qos: QoS::ExactlyOnce,
|
||||
retain: false,
|
||||
topic: ByteString::try_from(Bytes::from_static(b"topic")).unwrap(),
|
||||
message: Bytes::from(&b"message"[..]),
|
||||
}),
|
||||
username: None,
|
||||
password: None,
|
||||
}))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
decode_connect_packet(&mut Bytes::from_static(b"\x00\x02MQ00000000000000000000")),
|
||||
Err(ParseError::InvalidProtocol),
|
||||
);
|
||||
assert_eq!(
|
||||
decode_connect_packet(&mut Bytes::from_static(b"\x00\x10MQ00000000000000000000")),
|
||||
Err(ParseError::InvalidProtocol),
|
||||
);
|
||||
assert_eq!(
|
||||
decode_connect_packet(&mut Bytes::from_static(b"\x00\x04MQAA00000000000000000000")),
|
||||
Err(ParseError::InvalidProtocol),
|
||||
);
|
||||
assert_eq!(
|
||||
decode_connect_packet(&mut Bytes::from_static(
|
||||
b"\x00\x04MQTT\x0300000000000000000000"
|
||||
)),
|
||||
Err(ParseError::UnsupportedProtocolLevel),
|
||||
);
|
||||
assert_eq!(
|
||||
decode_connect_packet(&mut Bytes::from_static(
|
||||
b"\x00\x04MQTT\x04\xff00000000000000000000"
|
||||
)),
|
||||
Err(ParseError::ConnectReservedFlagSet)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
decode_connect_ack_packet(&mut Bytes::from_static(b"\x01\x04")),
|
||||
Ok(Packet::ConnectAck {
|
||||
session_present: true,
|
||||
return_code: ConnectCode::BadUserNameOrPassword
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
decode_connect_ack_packet(&mut Bytes::from_static(b"\x03\x04")),
|
||||
Err(ParseError::ConnAckReservedFlagSet)
|
||||
);
|
||||
|
||||
assert_decode_packet!(
|
||||
b"\x20\x02\x01\x04",
|
||||
Packet::ConnectAck {
|
||||
session_present: true,
|
||||
return_code: ConnectCode::BadUserNameOrPassword,
|
||||
}
|
||||
);
|
||||
|
||||
assert_decode_packet!(b"\xe0\x00", Packet::Disconnect);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_publish_packets() {
|
||||
//assert_eq!(
|
||||
// decode_publish_packet(b"\x00\x05topic\x12\x34"),
|
||||
// Done(&b""[..], ("topic".to_owned(), 0x1234))
|
||||
//);
|
||||
|
||||
assert_decode_packet!(
|
||||
b"\x3d\x0D\x00\x05topic\x43\x21data",
|
||||
Packet::Publish(Publish {
|
||||
dup: true,
|
||||
retain: true,
|
||||
qos: QoS::ExactlyOnce,
|
||||
topic: ByteString::try_from(Bytes::from_static(b"topic")).unwrap(),
|
||||
packet_id: Some(packet_id(0x4321)),
|
||||
payload: Bytes::from_static(b"data"),
|
||||
})
|
||||
);
|
||||
assert_decode_packet!(
|
||||
b"\x30\x0b\x00\x05topicdata",
|
||||
Packet::Publish(Publish {
|
||||
dup: false,
|
||||
retain: false,
|
||||
qos: QoS::AtMostOnce,
|
||||
topic: ByteString::try_from(Bytes::from_static(b"topic")).unwrap(),
|
||||
packet_id: None,
|
||||
payload: Bytes::from_static(b"data"),
|
||||
})
|
||||
);
|
||||
|
||||
assert_decode_packet!(
|
||||
b"\x40\x02\x43\x21",
|
||||
Packet::PublishAck {
|
||||
packet_id: packet_id(0x4321),
|
||||
}
|
||||
);
|
||||
assert_decode_packet!(
|
||||
b"\x50\x02\x43\x21",
|
||||
Packet::PublishReceived {
|
||||
packet_id: packet_id(0x4321),
|
||||
}
|
||||
);
|
||||
assert_decode_packet!(
|
||||
b"\x60\x02\x43\x21",
|
||||
Packet::PublishRelease {
|
||||
packet_id: packet_id(0x4321),
|
||||
}
|
||||
);
|
||||
assert_decode_packet!(
|
||||
b"\x70\x02\x43\x21",
|
||||
Packet::PublishComplete {
|
||||
packet_id: packet_id(0x4321),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_subscribe_packets() {
|
||||
let p = Packet::Subscribe {
|
||||
packet_id: packet_id(0x1234),
|
||||
topic_filters: vec![
|
||||
(
|
||||
ByteString::try_from(Bytes::from_static(b"test")).unwrap(),
|
||||
QoS::AtLeastOnce,
|
||||
),
|
||||
(
|
||||
ByteString::try_from(Bytes::from_static(b"filter")).unwrap(),
|
||||
QoS::ExactlyOnce,
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
decode_subscribe_packet(&mut Bytes::from_static(
|
||||
b"\x12\x34\x00\x04test\x01\x00\x06filter\x02"
|
||||
)),
|
||||
Ok(p.clone())
|
||||
);
|
||||
assert_decode_packet!(b"\x82\x12\x12\x34\x00\x04test\x01\x00\x06filter\x02", p);
|
||||
|
||||
let p = Packet::SubscribeAck {
|
||||
packet_id: packet_id(0x1234),
|
||||
status: vec![
|
||||
SubscribeReturnCode::Success(QoS::AtLeastOnce),
|
||||
SubscribeReturnCode::Failure,
|
||||
SubscribeReturnCode::Success(QoS::ExactlyOnce),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
decode_subscribe_ack_packet(&mut Bytes::from_static(b"\x12\x34\x01\x80\x02")),
|
||||
Ok(p.clone())
|
||||
);
|
||||
assert_decode_packet!(b"\x90\x05\x12\x34\x01\x80\x02", p);
|
||||
|
||||
let p = Packet::Unsubscribe {
|
||||
packet_id: packet_id(0x1234),
|
||||
topic_filters: vec![
|
||||
ByteString::try_from(Bytes::from_static(b"test")).unwrap(),
|
||||
ByteString::try_from(Bytes::from_static(b"filter")).unwrap(),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
decode_unsubscribe_packet(&mut Bytes::from_static(
|
||||
b"\x12\x34\x00\x04test\x00\x06filter"
|
||||
)),
|
||||
Ok(p.clone())
|
||||
);
|
||||
assert_decode_packet!(b"\xa2\x10\x12\x34\x00\x04test\x00\x06filter", p);
|
||||
|
||||
assert_decode_packet!(
|
||||
b"\xb0\x02\x43\x21",
|
||||
Packet::UnsubscribeAck {
|
||||
packet_id: packet_id(0x4321),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_ping_packets() {
|
||||
assert_decode_packet!(b"\xc0\x00", Packet::PingRequest);
|
||||
assert_decode_packet!(b"\xd0\x00", Packet::PingResponse);
|
||||
}
|
||||
}
|
446
actix-mqtt/codec/src/codec/encode.rs
Executable file
446
actix-mqtt/codec/src/codec/encode.rs
Executable file
@@ -0,0 +1,446 @@
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
use crate::packet::*;
|
||||
use crate::proto::*;
|
||||
|
||||
use super::{ConnectFlags, WILL_QOS_SHIFT};
|
||||
|
||||
pub fn write_packet(packet: &Packet, dst: &mut BytesMut, content_size: usize) {
|
||||
write_fixed_header(packet, dst, content_size);
|
||||
write_content(packet, dst);
|
||||
}
|
||||
|
||||
pub fn get_encoded_size(packet: &Packet) -> usize {
|
||||
match *packet {
|
||||
Packet::Connect ( ref connect ) => {
|
||||
match *connect {
|
||||
Connect {ref last_will, ref client_id, ref username, ref password, ..} =>
|
||||
{
|
||||
// Protocol Name + Protocol Level + Connect Flags + Keep Alive
|
||||
let mut n = 2 + 4 + 1 + 1 + 2;
|
||||
|
||||
// Client Id
|
||||
n += 2 + client_id.len();
|
||||
|
||||
// Will Topic + Will Message
|
||||
if let Some(LastWill { ref topic, ref message, .. }) = *last_will {
|
||||
n += 2 + topic.len() + 2 + message.len();
|
||||
}
|
||||
|
||||
if let Some(ref s) = *username {
|
||||
n += 2 + s.len();
|
||||
}
|
||||
|
||||
if let Some(ref s) = *password {
|
||||
n += 2 + s.len();
|
||||
}
|
||||
|
||||
n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Packet::Publish( Publish{ qos, ref topic, ref payload, .. }) => {
|
||||
// Topic + Packet Id + Payload
|
||||
if qos == QoS::AtLeastOnce || qos == QoS::ExactlyOnce {
|
||||
4 + topic.len() + payload.len()
|
||||
} else {
|
||||
2 + topic.len() + payload.len()
|
||||
}
|
||||
}
|
||||
|
||||
Packet::ConnectAck { .. } | // Flags + Return Code
|
||||
Packet::PublishAck { .. } | // Packet Id
|
||||
Packet::PublishReceived { .. } | // Packet Id
|
||||
Packet::PublishRelease { .. } | // Packet Id
|
||||
Packet::PublishComplete { .. } | // Packet Id
|
||||
Packet::UnsubscribeAck { .. } => 2, // Packet Id
|
||||
|
||||
Packet::Subscribe { ref topic_filters, .. } => {
|
||||
2 + topic_filters.iter().fold(0, |acc, &(ref filter, _)| acc + 2 + filter.len() + 1)
|
||||
}
|
||||
|
||||
Packet::SubscribeAck { ref status, .. } => 2 + status.len(),
|
||||
|
||||
Packet::Unsubscribe { ref topic_filters, .. } => {
|
||||
2 + topic_filters.iter().fold(0, |acc, filter| acc + 2 + filter.len())
|
||||
}
|
||||
|
||||
Packet::PingRequest | Packet::PingResponse | Packet::Disconnect => 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_fixed_header(packet: &Packet, dst: &mut BytesMut, content_size: usize) {
|
||||
dst.put_u8((packet.packet_type() << 4) | packet.packet_flags());
|
||||
write_variable_length(content_size, dst);
|
||||
}
|
||||
|
||||
fn write_content(packet: &Packet, dst: &mut BytesMut) {
|
||||
match *packet {
|
||||
Packet::Connect(ref connect) => match *connect {
|
||||
Connect {
|
||||
protocol,
|
||||
clean_session,
|
||||
keep_alive,
|
||||
ref last_will,
|
||||
ref client_id,
|
||||
ref username,
|
||||
ref password,
|
||||
} => {
|
||||
write_slice(protocol.name().as_bytes(), dst);
|
||||
|
||||
let mut flags = ConnectFlags::empty();
|
||||
|
||||
if username.is_some() {
|
||||
flags |= ConnectFlags::USERNAME;
|
||||
}
|
||||
if password.is_some() {
|
||||
flags |= ConnectFlags::PASSWORD;
|
||||
}
|
||||
|
||||
if let Some(LastWill { qos, retain, .. }) = *last_will {
|
||||
flags |= ConnectFlags::WILL;
|
||||
|
||||
if retain {
|
||||
flags |= ConnectFlags::WILL_RETAIN;
|
||||
}
|
||||
|
||||
let b: u8 = qos as u8;
|
||||
|
||||
flags |= ConnectFlags::from_bits_truncate(b << WILL_QOS_SHIFT);
|
||||
}
|
||||
|
||||
if clean_session {
|
||||
flags |= ConnectFlags::CLEAN_SESSION;
|
||||
}
|
||||
|
||||
dst.put_slice(&[protocol.level(), flags.bits()]);
|
||||
|
||||
dst.put_u16(keep_alive);
|
||||
|
||||
write_slice(client_id.as_bytes(), dst);
|
||||
|
||||
if let Some(LastWill {
|
||||
ref topic,
|
||||
ref message,
|
||||
..
|
||||
}) = *last_will
|
||||
{
|
||||
write_slice(topic.as_bytes(), dst);
|
||||
write_slice(&message, dst);
|
||||
}
|
||||
|
||||
if let Some(ref s) = *username {
|
||||
write_slice(s.as_bytes(), dst);
|
||||
}
|
||||
|
||||
if let Some(ref s) = *password {
|
||||
write_slice(s, dst);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Packet::ConnectAck {
|
||||
session_present,
|
||||
return_code,
|
||||
} => {
|
||||
dst.put_slice(&[if session_present { 0x01 } else { 0x00 }, return_code as u8]);
|
||||
}
|
||||
|
||||
Packet::Publish(Publish {
|
||||
qos,
|
||||
ref topic,
|
||||
packet_id,
|
||||
ref payload,
|
||||
..
|
||||
}) => {
|
||||
write_slice(topic.as_bytes(), dst);
|
||||
|
||||
if qos == QoS::AtLeastOnce || qos == QoS::ExactlyOnce {
|
||||
dst.put_u16(packet_id.unwrap().into());
|
||||
}
|
||||
|
||||
dst.put(payload.as_ref());
|
||||
}
|
||||
|
||||
Packet::PublishAck { packet_id }
|
||||
| Packet::PublishReceived { packet_id }
|
||||
| Packet::PublishRelease { packet_id }
|
||||
| Packet::PublishComplete { packet_id }
|
||||
| Packet::UnsubscribeAck { packet_id } => {
|
||||
dst.put_u16(packet_id.into());
|
||||
}
|
||||
|
||||
Packet::Subscribe {
|
||||
packet_id,
|
||||
ref topic_filters,
|
||||
} => {
|
||||
dst.put_u16(packet_id.into());
|
||||
|
||||
for &(ref filter, qos) in topic_filters {
|
||||
write_slice(filter.as_ref(), dst);
|
||||
dst.put_slice(&[qos as u8]);
|
||||
}
|
||||
}
|
||||
|
||||
Packet::SubscribeAck {
|
||||
packet_id,
|
||||
ref status,
|
||||
} => {
|
||||
dst.put_u16(packet_id.into());
|
||||
|
||||
let buf: Vec<u8> = status
|
||||
.iter()
|
||||
.map(|s| {
|
||||
if let SubscribeReturnCode::Success(qos) = *s {
|
||||
qos as u8
|
||||
} else {
|
||||
0x80
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
dst.put_slice(&buf);
|
||||
}
|
||||
|
||||
Packet::Unsubscribe {
|
||||
packet_id,
|
||||
ref topic_filters,
|
||||
} => {
|
||||
dst.put_u16(packet_id.into());
|
||||
|
||||
for filter in topic_filters {
|
||||
write_slice(filter.as_ref(), dst);
|
||||
}
|
||||
}
|
||||
|
||||
Packet::PingRequest | Packet::PingResponse | Packet::Disconnect => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_slice(r: &[u8], dst: &mut BytesMut) {
|
||||
dst.put_u16(r.len() as u16);
|
||||
dst.put_slice(r);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_variable_length(size: usize, dst: &mut BytesMut) {
|
||||
// todo: verify at higher level
|
||||
// if size > MAX_VARIABLE_LENGTH {
|
||||
// Err(Error::new(ErrorKind::Other, "out of range"))
|
||||
if size <= 127 {
|
||||
dst.put_u8(size as u8);
|
||||
} else if size <= 16383 {
|
||||
// 127 + 127 << 7
|
||||
dst.put_slice(&[((size % 128) | 0x80) as u8, (size >> 7) as u8]);
|
||||
} else if size <= 2_097_151 {
|
||||
// 127 + 127 << 7 + 127 << 14
|
||||
dst.put_slice(&[
|
||||
((size % 128) | 0x80) as u8,
|
||||
(((size >> 7) % 128) | 0x80) as u8,
|
||||
(size >> 14) as u8,
|
||||
]);
|
||||
} else {
|
||||
dst.put_slice(&[
|
||||
((size % 128) | 0x80) as u8,
|
||||
(((size >> 7) % 128) | 0x80) as u8,
|
||||
(((size >> 14) % 128) | 0x80) as u8,
|
||||
(size >> 21) as u8,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bytes::Bytes;
|
||||
use bytestring::ByteString;
|
||||
use std::num::NonZeroU16;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn packet_id(v: u16) -> NonZeroU16 {
|
||||
NonZeroU16::new(v).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_variable_length() {
|
||||
let mut v = BytesMut::new();
|
||||
|
||||
write_variable_length(123, &mut v);
|
||||
assert_eq!(v, [123].as_ref());
|
||||
|
||||
v.clear();
|
||||
|
||||
write_variable_length(129, &mut v);
|
||||
assert_eq!(v, b"\x81\x01".as_ref());
|
||||
|
||||
v.clear();
|
||||
|
||||
write_variable_length(16383, &mut v);
|
||||
assert_eq!(v, b"\xff\x7f".as_ref());
|
||||
|
||||
v.clear();
|
||||
|
||||
write_variable_length(2097151, &mut v);
|
||||
assert_eq!(v, b"\xff\xff\x7f".as_ref());
|
||||
|
||||
v.clear();
|
||||
|
||||
write_variable_length(268435455, &mut v);
|
||||
assert_eq!(v, b"\xff\xff\xff\x7f".as_ref());
|
||||
|
||||
// assert!(v.write_variable_length(MAX_VARIABLE_LENGTH + 1).is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_fixed_header() {
|
||||
let mut v = BytesMut::new();
|
||||
let p = Packet::PingRequest;
|
||||
|
||||
assert_eq!(get_encoded_size(&p), 0);
|
||||
write_fixed_header(&p, &mut v, 0);
|
||||
assert_eq!(v, b"\xc0\x00".as_ref());
|
||||
|
||||
v.clear();
|
||||
|
||||
let p = Packet::Publish(Publish {
|
||||
dup: true,
|
||||
retain: true,
|
||||
qos: QoS::ExactlyOnce,
|
||||
topic: ByteString::from_static("topic"),
|
||||
packet_id: Some(packet_id(0x4321)),
|
||||
payload: (0..255).collect::<Vec<u8>>().into(),
|
||||
});
|
||||
|
||||
assert_eq!(get_encoded_size(&p), 264);
|
||||
write_fixed_header(&p, &mut v, 264);
|
||||
assert_eq!(v, b"\x3d\x88\x02".as_ref());
|
||||
}
|
||||
|
||||
macro_rules! assert_packet {
|
||||
($p:expr, $data:expr) => {
|
||||
let mut v = BytesMut::with_capacity(1024);
|
||||
write_packet(&$p, &mut v, get_encoded_size($p));
|
||||
assert_eq!(v.len(), $data.len());
|
||||
assert_eq!(v, &$data[..]);
|
||||
// assert_eq!(read_packet($data.cursor()).unwrap(), (&b""[..], $p));
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_connect_packets() {
|
||||
assert_packet!(
|
||||
&Packet::Connect(Connect {
|
||||
protocol: Protocol::MQTT(4),
|
||||
clean_session: false,
|
||||
keep_alive: 60,
|
||||
client_id: ByteString::from_static("12345"),
|
||||
last_will: None,
|
||||
username: Some(ByteString::from_static("user")),
|
||||
password: Some(Bytes::from_static(b"pass")),
|
||||
}),
|
||||
&b"\x10\x1D\x00\x04MQTT\x04\xC0\x00\x3C\x00\
|
||||
\x0512345\x00\x04user\x00\x04pass"[..]
|
||||
);
|
||||
|
||||
assert_packet!(
|
||||
&Packet::Connect(Connect {
|
||||
protocol: Protocol::MQTT(4),
|
||||
clean_session: false,
|
||||
keep_alive: 60,
|
||||
client_id: ByteString::from_static("12345"),
|
||||
last_will: Some(LastWill {
|
||||
qos: QoS::ExactlyOnce,
|
||||
retain: false,
|
||||
topic: ByteString::from_static("topic"),
|
||||
message: Bytes::from_static(b"message"),
|
||||
}),
|
||||
username: None,
|
||||
password: None,
|
||||
}),
|
||||
&b"\x10\x21\x00\x04MQTT\x04\x14\x00\x3C\x00\
|
||||
\x0512345\x00\x05topic\x00\x07message"[..]
|
||||
);
|
||||
|
||||
assert_packet!(&Packet::Disconnect, b"\xe0\x00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_publish_packets() {
|
||||
assert_packet!(
|
||||
&Packet::Publish(Publish {
|
||||
dup: true,
|
||||
retain: true,
|
||||
qos: QoS::ExactlyOnce,
|
||||
topic: ByteString::from_static("topic"),
|
||||
packet_id: Some(packet_id(0x4321)),
|
||||
payload: Bytes::from_static(b"data"),
|
||||
}),
|
||||
b"\x3d\x0D\x00\x05topic\x43\x21data"
|
||||
);
|
||||
|
||||
assert_packet!(
|
||||
&Packet::Publish(Publish {
|
||||
dup: false,
|
||||
retain: false,
|
||||
qos: QoS::AtMostOnce,
|
||||
topic: ByteString::from_static("topic"),
|
||||
packet_id: None,
|
||||
payload: Bytes::from_static(b"data"),
|
||||
}),
|
||||
b"\x30\x0b\x00\x05topicdata"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_subscribe_packets() {
|
||||
assert_packet!(
|
||||
&Packet::Subscribe {
|
||||
packet_id: packet_id(0x1234),
|
||||
topic_filters: vec![
|
||||
(ByteString::from_static("test"), QoS::AtLeastOnce),
|
||||
(ByteString::from_static("filter"), QoS::ExactlyOnce)
|
||||
],
|
||||
},
|
||||
b"\x82\x12\x12\x34\x00\x04test\x01\x00\x06filter\x02"
|
||||
);
|
||||
|
||||
assert_packet!(
|
||||
&Packet::SubscribeAck {
|
||||
packet_id: packet_id(0x1234),
|
||||
status: vec![
|
||||
SubscribeReturnCode::Success(QoS::AtLeastOnce),
|
||||
SubscribeReturnCode::Failure,
|
||||
SubscribeReturnCode::Success(QoS::ExactlyOnce)
|
||||
],
|
||||
},
|
||||
b"\x90\x05\x12\x34\x01\x80\x02"
|
||||
);
|
||||
|
||||
assert_packet!(
|
||||
&Packet::Unsubscribe {
|
||||
packet_id: packet_id(0x1234),
|
||||
topic_filters: vec![
|
||||
ByteString::from_static("test"),
|
||||
ByteString::from_static("filter"),
|
||||
],
|
||||
},
|
||||
b"\xa2\x10\x12\x34\x00\x04test\x00\x06filter"
|
||||
);
|
||||
|
||||
assert_packet!(
|
||||
&Packet::UnsubscribeAck {
|
||||
packet_id: packet_id(0x4321)
|
||||
},
|
||||
b"\xb0\x02\x43\x21"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_ping_packets() {
|
||||
assert_packet!(&Packet::PingRequest, b"\xc0\x00");
|
||||
assert_packet!(&Packet::PingResponse, b"\xd0\x00");
|
||||
}
|
||||
}
|
161
actix-mqtt/codec/src/codec/mod.rs
Executable file
161
actix-mqtt/codec/src/codec/mod.rs
Executable file
@@ -0,0 +1,161 @@
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
use bytes::{buf::Buf, BytesMut};
|
||||
|
||||
use crate::error::ParseError;
|
||||
use crate::proto::QoS;
|
||||
use crate::{Packet, Publish};
|
||||
|
||||
mod decode;
|
||||
mod encode;
|
||||
|
||||
use self::decode::*;
|
||||
use self::encode::*;
|
||||
|
||||
bitflags! {
|
||||
pub struct ConnectFlags: u8 {
|
||||
const USERNAME = 0b1000_0000;
|
||||
const PASSWORD = 0b0100_0000;
|
||||
const WILL_RETAIN = 0b0010_0000;
|
||||
const WILL_QOS = 0b0001_1000;
|
||||
const WILL = 0b0000_0100;
|
||||
const CLEAN_SESSION = 0b0000_0010;
|
||||
}
|
||||
}
|
||||
|
||||
pub const WILL_QOS_SHIFT: u8 = 3;
|
||||
|
||||
bitflags! {
|
||||
pub struct ConnectAckFlags: u8 {
|
||||
const SESSION_PRESENT = 0b0000_0001;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Codec {
|
||||
state: DecodeState,
|
||||
max_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum DecodeState {
|
||||
FrameHeader,
|
||||
Frame(FixedHeader),
|
||||
}
|
||||
|
||||
impl Codec {
|
||||
/// Create `Codec` instance
|
||||
pub fn new() -> Self {
|
||||
Codec {
|
||||
state: DecodeState::FrameHeader,
|
||||
max_size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set max inbound frame size.
|
||||
///
|
||||
/// If max size is set to `0`, size is unlimited.
|
||||
/// By default max size is set to `0`
|
||||
pub fn max_size(mut self, size: usize) -> Self {
|
||||
self.max_size = size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Codec {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for Codec {
|
||||
type Item = Packet;
|
||||
type Error = ParseError;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, ParseError> {
|
||||
loop {
|
||||
match self.state {
|
||||
DecodeState::FrameHeader => {
|
||||
if src.len() < 2 {
|
||||
return Ok(None);
|
||||
}
|
||||
let fixed = src.as_ref()[0];
|
||||
match decode_variable_length(&src.as_ref()[1..])? {
|
||||
Some((remaining_length, consumed)) => {
|
||||
// check max message size
|
||||
if self.max_size != 0 && self.max_size < remaining_length {
|
||||
return Err(ParseError::MaxSizeExceeded);
|
||||
}
|
||||
src.advance(consumed + 1);
|
||||
self.state = DecodeState::Frame(FixedHeader {
|
||||
packet_type: fixed >> 4,
|
||||
packet_flags: fixed & 0xF,
|
||||
remaining_length,
|
||||
});
|
||||
// todo: validate remaining_length against max frame size config
|
||||
if src.len() < remaining_length {
|
||||
// todo: subtract?
|
||||
src.reserve(remaining_length); // extend receiving buffer to fit the whole frame -- todo: too eager?
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
DecodeState::Frame(fixed) => {
|
||||
if src.len() < fixed.remaining_length {
|
||||
return Ok(None);
|
||||
}
|
||||
let packet_buf = src.split_to(fixed.remaining_length);
|
||||
let packet = read_packet(packet_buf.freeze(), fixed)?;
|
||||
self.state = DecodeState::FrameHeader;
|
||||
src.reserve(2);
|
||||
return Ok(Some(packet));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder for Codec {
|
||||
type Item = Packet;
|
||||
type Error = ParseError;
|
||||
|
||||
fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), ParseError> {
|
||||
if let Packet::Publish(Publish { qos, packet_id, .. }) = item {
|
||||
if (qos == QoS::AtLeastOnce || qos == QoS::ExactlyOnce) && packet_id.is_none() {
|
||||
return Err(ParseError::PacketIdRequired);
|
||||
}
|
||||
}
|
||||
let content_size = get_encoded_size(&item);
|
||||
dst.reserve(content_size + 5);
|
||||
write_packet(&item, dst, content_size);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub(crate) struct FixedHeader {
|
||||
/// MQTT Control Packet type
|
||||
pub packet_type: u8,
|
||||
/// Flags specific to each MQTT Control Packet type
|
||||
pub packet_flags: u8,
|
||||
/// the number of bytes remaining within the current packet,
|
||||
/// including data in the variable header and the payload.
|
||||
pub remaining_length: usize,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_max_size() {
|
||||
let mut codec = Codec::new().max_size(5);
|
||||
|
||||
let mut buf = BytesMut::new();
|
||||
buf.extend_from_slice(b"\0\x09");
|
||||
assert_eq!(codec.decode(&mut buf), Err(ParseError::MaxSizeExceeded));
|
||||
}
|
||||
}
|
57
actix-mqtt/codec/src/error.rs
Executable file
57
actix-mqtt/codec/src/error.rs
Executable file
@@ -0,0 +1,57 @@
|
||||
use std::{io, str};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ParseError {
|
||||
InvalidProtocol,
|
||||
InvalidLength,
|
||||
MalformedPacket,
|
||||
UnsupportedProtocolLevel,
|
||||
ConnectReservedFlagSet,
|
||||
ConnAckReservedFlagSet,
|
||||
InvalidClientId,
|
||||
UnsupportedPacketType,
|
||||
PacketIdRequired,
|
||||
MaxSizeExceeded,
|
||||
IoError(io::Error),
|
||||
Utf8Error(str::Utf8Error),
|
||||
}
|
||||
|
||||
impl PartialEq for ParseError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(ParseError::InvalidProtocol, ParseError::InvalidProtocol) => true,
|
||||
(ParseError::InvalidLength, ParseError::InvalidLength) => true,
|
||||
(ParseError::UnsupportedProtocolLevel, ParseError::UnsupportedProtocolLevel) => {
|
||||
true
|
||||
}
|
||||
(ParseError::ConnectReservedFlagSet, ParseError::ConnectReservedFlagSet) => true,
|
||||
(ParseError::ConnAckReservedFlagSet, ParseError::ConnAckReservedFlagSet) => true,
|
||||
(ParseError::InvalidClientId, ParseError::InvalidClientId) => true,
|
||||
(ParseError::UnsupportedPacketType, ParseError::UnsupportedPacketType) => true,
|
||||
(ParseError::PacketIdRequired, ParseError::PacketIdRequired) => true,
|
||||
(ParseError::MaxSizeExceeded, ParseError::MaxSizeExceeded) => true,
|
||||
(ParseError::MalformedPacket, ParseError::MalformedPacket) => true,
|
||||
(ParseError::IoError(_), _) => false,
|
||||
(ParseError::Utf8Error(_), _) => false,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for ParseError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
ParseError::IoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<str::Utf8Error> for ParseError {
|
||||
fn from(err: str::Utf8Error) -> Self {
|
||||
ParseError::Utf8Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum TopicError {
|
||||
InvalidTopic,
|
||||
InvalidLevel,
|
||||
}
|
22
actix-mqtt/codec/src/lib.rs
Executable file
22
actix-mqtt/codec/src/lib.rs
Executable file
@@ -0,0 +1,22 @@
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
|
||||
extern crate bytestring;
|
||||
|
||||
mod error;
|
||||
#[macro_use]
|
||||
mod topic;
|
||||
#[macro_use]
|
||||
mod proto;
|
||||
mod codec;
|
||||
mod packet;
|
||||
|
||||
pub use self::codec::Codec;
|
||||
pub use self::error::{ParseError, TopicError};
|
||||
pub use self::packet::{Connect, ConnectCode, LastWill, Packet, Publish, SubscribeReturnCode};
|
||||
pub use self::proto::{Protocol, QoS};
|
||||
pub use self::topic::{Level, Topic};
|
||||
|
||||
// http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||
pub const TCP_PORT: u16 = 1883;
|
||||
pub const SSL_PORT: u16 = 8883;
|
251
actix-mqtt/codec/src/packet.rs
Executable file
251
actix-mqtt/codec/src/packet.rs
Executable file
@@ -0,0 +1,251 @@
|
||||
use bytes::Bytes;
|
||||
use bytestring::ByteString;
|
||||
use std::num::NonZeroU16;
|
||||
|
||||
use crate::proto::{Protocol, QoS};
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
/// Connect Return Code
|
||||
pub enum ConnectCode {
|
||||
/// Connection accepted
|
||||
ConnectionAccepted = 0,
|
||||
/// Connection Refused, unacceptable protocol version
|
||||
UnacceptableProtocolVersion = 1,
|
||||
/// Connection Refused, identifier rejected
|
||||
IdentifierRejected = 2,
|
||||
/// Connection Refused, Server unavailable
|
||||
ServiceUnavailable = 3,
|
||||
/// Connection Refused, bad user name or password
|
||||
BadUserNameOrPassword = 4,
|
||||
/// Connection Refused, not authorized
|
||||
NotAuthorized = 5,
|
||||
/// Reserved
|
||||
Reserved = 6,
|
||||
}
|
||||
|
||||
const_enum!(ConnectCode: u8);
|
||||
|
||||
impl ConnectCode {
|
||||
pub fn reason(self) -> &'static str {
|
||||
match self {
|
||||
ConnectCode::ConnectionAccepted => "Connection Accepted",
|
||||
ConnectCode::UnacceptableProtocolVersion => {
|
||||
"Connection Refused, unacceptable protocol version"
|
||||
}
|
||||
ConnectCode::IdentifierRejected => "Connection Refused, identifier rejected",
|
||||
ConnectCode::ServiceUnavailable => "Connection Refused, Server unavailable",
|
||||
ConnectCode::BadUserNameOrPassword => {
|
||||
"Connection Refused, bad user name or password"
|
||||
}
|
||||
ConnectCode::NotAuthorized => "Connection Refused, not authorized",
|
||||
_ => "Connection Refused",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
/// Connection Will
|
||||
pub struct LastWill {
|
||||
/// the QoS level to be used when publishing the Will Message.
|
||||
pub qos: QoS,
|
||||
/// the Will Message is to be Retained when it is published.
|
||||
pub retain: bool,
|
||||
/// the Will Topic
|
||||
pub topic: ByteString,
|
||||
/// defines the Application Message that is to be published to the Will Topic
|
||||
pub message: Bytes,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
/// Connect packet content
|
||||
pub struct Connect {
|
||||
/// mqtt protocol version
|
||||
pub protocol: Protocol,
|
||||
/// the handling of the Session state.
|
||||
pub clean_session: bool,
|
||||
/// a time interval measured in seconds.
|
||||
pub keep_alive: u16,
|
||||
/// Will Message be stored on the Server and associated with the Network Connection.
|
||||
pub last_will: Option<LastWill>,
|
||||
/// identifies the Client to the Server.
|
||||
pub client_id: ByteString,
|
||||
/// username can be used by the Server for authentication and authorization.
|
||||
pub username: Option<ByteString>,
|
||||
/// password can be used by the Server for authentication and authorization.
|
||||
pub password: Option<Bytes>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
/// Publish message
|
||||
pub struct Publish {
|
||||
/// this might be re-delivery of an earlier attempt to send the Packet.
|
||||
pub dup: bool,
|
||||
pub retain: bool,
|
||||
/// the level of assurance for delivery of an Application Message.
|
||||
pub qos: QoS,
|
||||
/// the information channel to which payload data is published.
|
||||
pub topic: ByteString,
|
||||
/// only present in PUBLISH Packets where the QoS level is 1 or 2.
|
||||
pub packet_id: Option<NonZeroU16>,
|
||||
/// the Application Message that is being published.
|
||||
pub payload: Bytes,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
/// Subscribe Return Code
|
||||
pub enum SubscribeReturnCode {
|
||||
Success(QoS),
|
||||
Failure,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
/// MQTT Control Packets
|
||||
pub enum Packet {
|
||||
/// Client request to connect to Server
|
||||
Connect(Connect),
|
||||
|
||||
/// Connect acknowledgment
|
||||
ConnectAck {
|
||||
/// enables a Client to establish whether the Client and Server have a consistent view
|
||||
/// about whether there is already stored Session state.
|
||||
session_present: bool,
|
||||
return_code: ConnectCode,
|
||||
},
|
||||
|
||||
/// Publish message
|
||||
Publish(Publish),
|
||||
|
||||
/// Publish acknowledgment
|
||||
PublishAck {
|
||||
/// Packet Identifier
|
||||
packet_id: NonZeroU16,
|
||||
},
|
||||
/// Publish received (assured delivery part 1)
|
||||
PublishReceived {
|
||||
/// Packet Identifier
|
||||
packet_id: NonZeroU16,
|
||||
},
|
||||
/// Publish release (assured delivery part 2)
|
||||
PublishRelease {
|
||||
/// Packet Identifier
|
||||
packet_id: NonZeroU16,
|
||||
},
|
||||
/// Publish complete (assured delivery part 3)
|
||||
PublishComplete {
|
||||
/// Packet Identifier
|
||||
packet_id: NonZeroU16,
|
||||
},
|
||||
|
||||
/// Client subscribe request
|
||||
Subscribe {
|
||||
/// Packet Identifier
|
||||
packet_id: NonZeroU16,
|
||||
/// the list of Topic Filters and QoS to which the Client wants to subscribe.
|
||||
topic_filters: Vec<(ByteString, QoS)>,
|
||||
},
|
||||
/// Subscribe acknowledgment
|
||||
SubscribeAck {
|
||||
packet_id: NonZeroU16,
|
||||
/// corresponds to a Topic Filter in the SUBSCRIBE Packet being acknowledged.
|
||||
status: Vec<SubscribeReturnCode>,
|
||||
},
|
||||
|
||||
/// Unsubscribe request
|
||||
Unsubscribe {
|
||||
/// Packet Identifier
|
||||
packet_id: NonZeroU16,
|
||||
/// the list of Topic Filters that the Client wishes to unsubscribe from.
|
||||
topic_filters: Vec<ByteString>,
|
||||
},
|
||||
/// Unsubscribe acknowledgment
|
||||
UnsubscribeAck {
|
||||
/// Packet Identifier
|
||||
packet_id: NonZeroU16,
|
||||
},
|
||||
|
||||
/// PING request
|
||||
PingRequest,
|
||||
/// PING response
|
||||
PingResponse,
|
||||
|
||||
/// Client is disconnecting
|
||||
Disconnect,
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
#[inline]
|
||||
/// MQTT Control Packet type
|
||||
pub fn packet_type(&self) -> u8 {
|
||||
match *self {
|
||||
Packet::Connect { .. } => CONNECT,
|
||||
Packet::ConnectAck { .. } => CONNACK,
|
||||
Packet::Publish { .. } => PUBLISH,
|
||||
Packet::PublishAck { .. } => PUBACK,
|
||||
Packet::PublishReceived { .. } => PUBREC,
|
||||
Packet::PublishRelease { .. } => PUBREL,
|
||||
Packet::PublishComplete { .. } => PUBCOMP,
|
||||
Packet::Subscribe { .. } => SUBSCRIBE,
|
||||
Packet::SubscribeAck { .. } => SUBACK,
|
||||
Packet::Unsubscribe { .. } => UNSUBSCRIBE,
|
||||
Packet::UnsubscribeAck { .. } => UNSUBACK,
|
||||
Packet::PingRequest => PINGREQ,
|
||||
Packet::PingResponse => PINGRESP,
|
||||
Packet::Disconnect => DISCONNECT,
|
||||
}
|
||||
}
|
||||
|
||||
/// Flags specific to each MQTT Control Packet type
|
||||
pub fn packet_flags(&self) -> u8 {
|
||||
match *self {
|
||||
Packet::Publish(Publish {
|
||||
dup, qos, retain, ..
|
||||
}) => {
|
||||
let mut b = qos as u8;
|
||||
|
||||
b <<= 1;
|
||||
|
||||
if dup {
|
||||
b |= 0b1000;
|
||||
}
|
||||
|
||||
if retain {
|
||||
b |= 0b0001;
|
||||
}
|
||||
|
||||
b
|
||||
}
|
||||
Packet::PublishRelease { .. }
|
||||
| Packet::Subscribe { .. }
|
||||
| Packet::Unsubscribe { .. } => 0b0010,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Connect> for Packet {
|
||||
fn from(val: Connect) -> Packet {
|
||||
Packet::Connect(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Publish> for Packet {
|
||||
fn from(val: Publish) -> Packet {
|
||||
Packet::Publish(val)
|
||||
}
|
||||
}
|
||||
|
||||
pub const CONNECT: u8 = 1;
|
||||
pub const CONNACK: u8 = 2;
|
||||
pub const PUBLISH: u8 = 3;
|
||||
pub const PUBACK: u8 = 4;
|
||||
pub const PUBREC: u8 = 5;
|
||||
pub const PUBREL: u8 = 6;
|
||||
pub const PUBCOMP: u8 = 7;
|
||||
pub const SUBSCRIBE: u8 = 8;
|
||||
pub const SUBACK: u8 = 9;
|
||||
pub const UNSUBSCRIBE: u8 = 10;
|
||||
pub const UNSUBACK: u8 = 11;
|
||||
pub const PINGREQ: u8 = 12;
|
||||
pub const PINGRESP: u8 = 13;
|
||||
pub const DISCONNECT: u8 = 14;
|
64
actix-mqtt/codec/src/proto.rs
Executable file
64
actix-mqtt/codec/src/proto.rs
Executable file
@@ -0,0 +1,64 @@
|
||||
#[macro_export]
|
||||
macro_rules! const_enum {
|
||||
($name:ty : $repr:ty) => {
|
||||
impl ::std::convert::From<$repr> for $name {
|
||||
fn from(u: $repr) -> Self {
|
||||
unsafe { ::std::mem::transmute(u) }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const DEFAULT_MQTT_LEVEL: u8 = 4;
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Protocol {
|
||||
MQTT(u8),
|
||||
}
|
||||
|
||||
impl Protocol {
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Protocol::MQTT(_) => "MQTT",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn level(self) -> u8 {
|
||||
match self {
|
||||
Protocol::MQTT(level) => level,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Protocol {
|
||||
fn default() -> Self {
|
||||
Protocol::MQTT(DEFAULT_MQTT_LEVEL)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// Quality of Service levels
|
||||
pub enum QoS {
|
||||
/// At most once delivery
|
||||
///
|
||||
/// The message is delivered according to the capabilities of the underlying network.
|
||||
/// No response is sent by the receiver and no retry is performed by the sender.
|
||||
/// The message arrives at the receiver either once or not at all.
|
||||
AtMostOnce = 0,
|
||||
/// At least once delivery
|
||||
///
|
||||
/// This quality of service ensures that the message arrives at the receiver at least once.
|
||||
/// A QoS 1 PUBLISH Packet has a Packet Identifier in its variable header
|
||||
/// and is acknowledged by a PUBACK Packet.
|
||||
AtLeastOnce = 1,
|
||||
/// Exactly once delivery
|
||||
///
|
||||
/// This is the highest quality of service,
|
||||
/// for use when neither loss nor duplication of messages are acceptable.
|
||||
/// There is an increased overhead associated with this quality of service.
|
||||
ExactlyOnce = 2,
|
||||
}
|
||||
|
||||
const_enum!(QoS: u8);
|
520
actix-mqtt/codec/src/topic.rs
Executable file
520
actix-mqtt/codec/src/topic.rs
Executable file
@@ -0,0 +1,520 @@
|
||||
use std::fmt::{self, Write};
|
||||
use std::{io, ops, str::FromStr};
|
||||
|
||||
use crate::error::TopicError;
|
||||
|
||||
#[inline]
|
||||
fn is_metadata<T: AsRef<str>>(s: T) -> bool {
|
||||
s.as_ref().chars().nth(0) == Some('$')
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum Level {
|
||||
Normal(String),
|
||||
Metadata(String), // $SYS
|
||||
Blank,
|
||||
SingleWildcard, // Single level wildcard +
|
||||
MultiWildcard, // Multi-level wildcard #
|
||||
}
|
||||
|
||||
impl Level {
|
||||
pub fn parse<T: AsRef<str>>(s: T) -> Result<Level, TopicError> {
|
||||
Level::from_str(s.as_ref())
|
||||
}
|
||||
|
||||
pub fn normal<T: AsRef<str>>(s: T) -> Level {
|
||||
if s.as_ref().contains(|c| c == '+' || c == '#') {
|
||||
panic!("invalid normal level `{}` contains +|#", s.as_ref());
|
||||
}
|
||||
|
||||
if s.as_ref().chars().nth(0) == Some('$') {
|
||||
panic!("invalid normal level `{}` starts with $", s.as_ref())
|
||||
}
|
||||
|
||||
Level::Normal(String::from(s.as_ref()))
|
||||
}
|
||||
|
||||
pub fn metadata<T: AsRef<str>>(s: T) -> Level {
|
||||
if s.as_ref().contains(|c| c == '+' || c == '#') {
|
||||
panic!("invalid metadata level `{}` contains +|#", s.as_ref());
|
||||
}
|
||||
|
||||
if s.as_ref().chars().nth(0) != Some('$') {
|
||||
panic!("invalid metadata level `{}` not starts with $", s.as_ref())
|
||||
}
|
||||
|
||||
Level::Metadata(String::from(s.as_ref()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn value(&self) -> Option<&str> {
|
||||
match *self {
|
||||
Level::Normal(ref s) | Level::Metadata(ref s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_normal(&self) -> bool {
|
||||
if let Level::Normal(_) = *self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_metadata(&self) -> bool {
|
||||
if let Level::Metadata(_) = *self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
match *self {
|
||||
Level::Normal(ref s) => {
|
||||
s.chars().nth(0) != Some('$') && !s.contains(|c| c == '+' || c == '#')
|
||||
}
|
||||
Level::Metadata(ref s) => {
|
||||
s.chars().nth(0) == Some('$') && !s.contains(|c| c == '+' || c == '#')
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, Clone)]
|
||||
pub struct Topic(Vec<Level>);
|
||||
|
||||
impl Topic {
|
||||
#[inline]
|
||||
pub fn levels(&self) -> &Vec<Level> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.0
|
||||
.iter()
|
||||
.position(|level| !level.is_valid())
|
||||
.or_else(|| {
|
||||
self.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.position(|(pos, level)| match *level {
|
||||
Level::MultiWildcard => pos != self.0.len() - 1,
|
||||
Level::Metadata(_) => pos != 0,
|
||||
_ => false,
|
||||
})
|
||||
})
|
||||
.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! match_topic {
|
||||
($topic:expr, $levels:expr) => {{
|
||||
let mut lhs = $topic.0.iter();
|
||||
|
||||
for rhs in $levels {
|
||||
match lhs.next() {
|
||||
Some(&Level::SingleWildcard) => {
|
||||
if !rhs.match_level(&Level::SingleWildcard) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(&Level::MultiWildcard) => {
|
||||
return rhs.match_level(&Level::MultiWildcard);
|
||||
}
|
||||
Some(level) if rhs.match_level(level) => continue,
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
|
||||
match lhs.next() {
|
||||
Some(&Level::MultiWildcard) => true,
|
||||
Some(_) => false,
|
||||
None => true,
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
impl PartialEq for Topic {
|
||||
fn eq(&self, other: &Topic) -> bool {
|
||||
match_topic!(self, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> PartialEq<T> for Topic {
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
match_topic!(self, other.as_ref().split('/'))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [Level]> for Topic {
|
||||
fn from(s: &[Level]) -> Self {
|
||||
let mut v = vec![];
|
||||
|
||||
v.extend_from_slice(s);
|
||||
|
||||
Topic(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Level>> for Topic {
|
||||
fn from(v: Vec<Level>) -> Self {
|
||||
Topic(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Vec<Level>> for Topic {
|
||||
fn into(self) -> Vec<Level> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for Topic {
|
||||
type Target = Vec<Level>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for Topic {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! topic {
|
||||
($s:expr) => {
|
||||
$s.parse::<Topic>().unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
pub trait MatchLevel {
|
||||
fn match_level(&self, level: &Level) -> bool;
|
||||
}
|
||||
|
||||
impl MatchLevel for Level {
|
||||
fn match_level(&self, level: &Level) -> bool {
|
||||
match *level {
|
||||
Level::Normal(ref lhs) => {
|
||||
if let Level::Normal(ref rhs) = *self {
|
||||
lhs == rhs
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Level::Metadata(ref lhs) => {
|
||||
if let Level::Metadata(ref rhs) = *self {
|
||||
lhs == rhs
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Level::Blank => true,
|
||||
Level::SingleWildcard | Level::MultiWildcard => !self.is_metadata(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> MatchLevel for T {
|
||||
fn match_level(&self, level: &Level) -> bool {
|
||||
match *level {
|
||||
Level::Normal(ref lhs) => !is_metadata(self) && lhs == self.as_ref(),
|
||||
Level::Metadata(ref lhs) => is_metadata(self) && lhs == self.as_ref(),
|
||||
Level::Blank => self.as_ref().is_empty(),
|
||||
Level::SingleWildcard | Level::MultiWildcard => !is_metadata(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Level {
|
||||
type Err = TopicError;
|
||||
|
||||
#[inline]
|
||||
fn from_str(s: &str) -> Result<Self, TopicError> {
|
||||
match s {
|
||||
"+" => Ok(Level::SingleWildcard),
|
||||
"#" => Ok(Level::MultiWildcard),
|
||||
"" => Ok(Level::Blank),
|
||||
_ => {
|
||||
if s.contains(|c| c == '+' || c == '#') {
|
||||
Err(TopicError::InvalidLevel)
|
||||
} else if is_metadata(s) {
|
||||
Ok(Level::Metadata(String::from(s)))
|
||||
} else {
|
||||
Ok(Level::Normal(String::from(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Topic {
|
||||
type Err = TopicError;
|
||||
|
||||
#[inline]
|
||||
fn from_str(s: &str) -> Result<Self, TopicError> {
|
||||
s.split('/')
|
||||
.map(|level| Level::from_str(level))
|
||||
.collect::<Result<Vec<_>, TopicError>>()
|
||||
.map(Topic)
|
||||
.and_then(|topic| {
|
||||
if topic.is_valid() {
|
||||
Ok(topic)
|
||||
} else {
|
||||
Err(TopicError::InvalidTopic)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Level {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Level::Normal(ref s) | Level::Metadata(ref s) => f.write_str(s.as_str()),
|
||||
Level::Blank => Ok(()),
|
||||
Level::SingleWildcard => f.write_char('+'),
|
||||
Level::MultiWildcard => f.write_char('#'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Topic {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut first = true;
|
||||
|
||||
for level in &self.0 {
|
||||
if first {
|
||||
first = false;
|
||||
} else {
|
||||
f.write_char('/')?;
|
||||
}
|
||||
|
||||
level.fmt(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WriteTopicExt: io::Write {
|
||||
fn write_level(&mut self, level: &Level) -> io::Result<usize> {
|
||||
match *level {
|
||||
Level::Normal(ref s) | Level::Metadata(ref s) => self.write(s.as_str().as_bytes()),
|
||||
Level::Blank => Ok(0),
|
||||
Level::SingleWildcard => self.write(b"+"),
|
||||
Level::MultiWildcard => self.write(b"#"),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_topic(&mut self, topic: &Topic) -> io::Result<usize> {
|
||||
let mut n = 0;
|
||||
let mut first = true;
|
||||
|
||||
for level in topic.levels() {
|
||||
if first {
|
||||
first = false;
|
||||
} else {
|
||||
n += self.write(b"/")?;
|
||||
}
|
||||
|
||||
n += self.write_level(level)?;
|
||||
}
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: io::Write + ?Sized> WriteTopicExt for W {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_level() {
|
||||
assert!(Level::normal("sport").is_normal());
|
||||
assert!(Level::metadata("$SYS").is_metadata());
|
||||
|
||||
assert_eq!(Level::normal("sport").value(), Some("sport"));
|
||||
assert_eq!(Level::metadata("$SYS").value(), Some("$SYS"));
|
||||
|
||||
assert_eq!(Level::normal("sport"), "sport".parse().unwrap());
|
||||
assert_eq!(Level::metadata("$SYS"), "$SYS".parse().unwrap());
|
||||
|
||||
assert!(Level::Normal(String::from("sport")).is_valid());
|
||||
assert!(Level::Metadata(String::from("$SYS")).is_valid());
|
||||
|
||||
assert!(!Level::Normal(String::from("$sport")).is_valid());
|
||||
assert!(!Level::Metadata(String::from("SYS")).is_valid());
|
||||
|
||||
assert!(!Level::Normal(String::from("sport#")).is_valid());
|
||||
assert!(!Level::Metadata(String::from("SYS+")).is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_topic() {
|
||||
assert!(Topic(vec![
|
||||
Level::normal("sport"),
|
||||
Level::normal("tennis"),
|
||||
Level::normal("player1")
|
||||
])
|
||||
.is_valid());
|
||||
|
||||
assert!(Topic(vec![
|
||||
Level::normal("sport"),
|
||||
Level::normal("tennis"),
|
||||
Level::MultiWildcard
|
||||
])
|
||||
.is_valid());
|
||||
assert!(Topic(vec![
|
||||
Level::metadata("$SYS"),
|
||||
Level::normal("tennis"),
|
||||
Level::MultiWildcard
|
||||
])
|
||||
.is_valid());
|
||||
|
||||
assert!(Topic(vec![
|
||||
Level::normal("sport"),
|
||||
Level::SingleWildcard,
|
||||
Level::normal("player1")
|
||||
])
|
||||
.is_valid());
|
||||
|
||||
assert!(!Topic(vec![
|
||||
Level::normal("sport"),
|
||||
Level::MultiWildcard,
|
||||
Level::normal("player1")
|
||||
])
|
||||
.is_valid());
|
||||
assert!(!Topic(vec![
|
||||
Level::normal("sport"),
|
||||
Level::metadata("$SYS"),
|
||||
Level::normal("player1")
|
||||
])
|
||||
.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_topic() {
|
||||
assert_eq!(
|
||||
topic!("sport/tennis/player1"),
|
||||
Topic::from(vec![
|
||||
Level::normal("sport"),
|
||||
Level::normal("tennis"),
|
||||
Level::normal("player1")
|
||||
])
|
||||
);
|
||||
|
||||
assert_eq!(topic!(""), Topic(vec![Level::Blank]));
|
||||
assert_eq!(
|
||||
topic!("/finance"),
|
||||
Topic::from(vec![Level::Blank, Level::normal("finance")])
|
||||
);
|
||||
|
||||
assert_eq!(topic!("$SYS"), Topic::from(vec![Level::metadata("$SYS")]));
|
||||
assert!("sport/$SYS".parse::<Topic>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_wildcard_topic() {
|
||||
assert_eq!(
|
||||
topic!("sport/tennis/#"),
|
||||
Topic::from(vec![
|
||||
Level::normal("sport"),
|
||||
Level::normal("tennis"),
|
||||
Level::MultiWildcard
|
||||
])
|
||||
);
|
||||
|
||||
assert_eq!(topic!("#"), Topic::from(vec![Level::MultiWildcard]));
|
||||
assert!("sport/tennis#".parse::<Topic>().is_err());
|
||||
assert!("sport/tennis/#/ranking".parse::<Topic>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_wildcard_topic() {
|
||||
assert_eq!(topic!("+"), Topic::from(vec![Level::SingleWildcard]));
|
||||
|
||||
assert_eq!(
|
||||
topic!("+/tennis/#"),
|
||||
Topic::from(vec![
|
||||
Level::SingleWildcard,
|
||||
Level::normal("tennis"),
|
||||
Level::MultiWildcard
|
||||
])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
topic!("sport/+/player1"),
|
||||
Topic::from(vec![
|
||||
Level::normal("sport"),
|
||||
Level::SingleWildcard,
|
||||
Level::normal("player1")
|
||||
])
|
||||
);
|
||||
|
||||
assert!("sport+".parse::<Topic>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_topic() {
|
||||
let mut v = vec![];
|
||||
let t = vec![
|
||||
Level::SingleWildcard,
|
||||
Level::normal("tennis"),
|
||||
Level::MultiWildcard,
|
||||
]
|
||||
.into();
|
||||
|
||||
assert_eq!(v.write_topic(&t).unwrap(), 10);
|
||||
assert_eq!(v, b"+/tennis/#");
|
||||
|
||||
assert_eq!(format!("{}", t), "+/tennis/#");
|
||||
assert_eq!(t.to_string(), "+/tennis/#");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_topic() {
|
||||
assert!("test".match_level(&Level::normal("test")));
|
||||
assert!("$SYS".match_level(&Level::metadata("$SYS")));
|
||||
|
||||
let t: Topic = "sport/tennis/player1/#".parse().unwrap();
|
||||
|
||||
assert_eq!(t, "sport/tennis/player1");
|
||||
assert_eq!(t, "sport/tennis/player1/ranking");
|
||||
assert_eq!(t, "sport/tennis/player1/score/wimbledon");
|
||||
|
||||
assert_eq!(Topic::from_str("sport/#").unwrap(), "sport");
|
||||
|
||||
let t: Topic = "sport/tennis/+".parse().unwrap();
|
||||
|
||||
assert_eq!(t, "sport/tennis/player1");
|
||||
assert_eq!(t, "sport/tennis/player2");
|
||||
assert!(t != "sport/tennis/player1/ranking");
|
||||
|
||||
let t: Topic = "sport/+".parse().unwrap();
|
||||
|
||||
assert!(t != "sport");
|
||||
assert_eq!(t, "sport/");
|
||||
|
||||
assert_eq!(Topic::from_str("+/+").unwrap(), "/finance");
|
||||
assert_eq!(Topic::from_str("/+").unwrap(), "/finance",);
|
||||
assert!(Topic::from_str("+").unwrap() != "/finance",);
|
||||
|
||||
assert!(Topic::from_str("#").unwrap() != "$SYS");
|
||||
assert!(Topic::from_str("+/monitor/Clients").unwrap() != "$SYS/monitor/Clients");
|
||||
assert_eq!(Topic::from_str(&"$SYS/#").unwrap(), "$SYS/");
|
||||
assert_eq!(
|
||||
Topic::from_str("$SYS/monitor/+").unwrap(),
|
||||
"$SYS/monitor/Clients",
|
||||
);
|
||||
}
|
||||
}
|
35
actix-mqtt/examples/basic.rs
Executable file
35
actix-mqtt/examples/basic.rs
Executable file
@@ -0,0 +1,35 @@
|
||||
use actix_mqtt::{Connect, ConnectAck, MqttServer, Publish};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Session;
|
||||
|
||||
async fn connect<Io>(connect: Connect<Io>) -> Result<ConnectAck<Io, Session>, ()> {
|
||||
log::info!("new connection: {:?}", connect);
|
||||
Ok(connect.ack(Session, false))
|
||||
}
|
||||
|
||||
async fn publish(publish: Publish<Session>) -> Result<(), ()> {
|
||||
log::info!(
|
||||
"incoming publish: {:?} -> {:?}",
|
||||
publish.id(),
|
||||
publish.topic()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
"actix_server=trace,actix_mqtt=trace,basic=trace",
|
||||
);
|
||||
env_logger::init();
|
||||
|
||||
actix_server::Server::build()
|
||||
.bind("mqtt", "127.0.0.1:1883", || {
|
||||
MqttServer::new(connect).finish(publish)
|
||||
})?
|
||||
.workers(1)
|
||||
.run()
|
||||
.await
|
||||
}
|
64
actix-mqtt/src/cell.rs
Executable file
64
actix-mqtt/src/cell.rs
Executable file
@@ -0,0 +1,64 @@
|
||||
//! Custom cell impl
|
||||
use std::cell::UnsafeCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_service::Service;
|
||||
|
||||
pub(crate) struct Cell<T> {
|
||||
inner: Rc<UnsafeCell<T>>,
|
||||
}
|
||||
|
||||
impl<T> Clone for Cell<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Cell<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.get_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::fmt::Debug> std::fmt::Debug for Cell<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Cell<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner: Rc::new(UnsafeCell::new(inner)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ref(&self) -> &T {
|
||||
unsafe { &*self.inner.as_ref().get() }
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self) -> &mut T {
|
||||
unsafe { &mut *self.inner.as_ref().get() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Service> Service for Cell<T> {
|
||||
type Request = T::Request;
|
||||
type Response = T::Response;
|
||||
type Error = T::Error;
|
||||
type Future = T::Future;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), T::Error>> {
|
||||
self.get_mut().poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: T::Request) -> T::Future {
|
||||
self.get_mut().call(req)
|
||||
}
|
||||
}
|
406
actix-mqtt/src/client.rs
Executable file
406
actix-mqtt/src/client.rs
Executable file
@@ -0,0 +1,406 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_ioframe as ioframe;
|
||||
use actix_service::{boxed, IntoService, IntoServiceFactory, Service, ServiceFactory};
|
||||
use bytes::Bytes;
|
||||
use bytestring::ByteString;
|
||||
use futures::future::{FutureExt, LocalBoxFuture};
|
||||
use futures::{Sink, SinkExt, Stream, StreamExt};
|
||||
use mqtt_codec as mqtt;
|
||||
|
||||
use crate::cell::Cell;
|
||||
use crate::default::{SubsNotImplemented, UnsubsNotImplemented};
|
||||
use crate::dispatcher::{dispatcher, MqttState};
|
||||
use crate::error::MqttError;
|
||||
use crate::publish::Publish;
|
||||
use crate::sink::MqttSink;
|
||||
use crate::subs::{Subscribe, SubscribeResult, Unsubscribe};
|
||||
|
||||
/// Mqtt client
|
||||
#[derive(Clone)]
|
||||
pub struct Client<Io, St> {
|
||||
client_id: ByteString,
|
||||
clean_session: bool,
|
||||
protocol: mqtt::Protocol,
|
||||
keep_alive: u16,
|
||||
last_will: Option<mqtt::LastWill>,
|
||||
username: Option<ByteString>,
|
||||
password: Option<Bytes>,
|
||||
inflight: usize,
|
||||
_t: PhantomData<(Io, St)>,
|
||||
}
|
||||
|
||||
impl<Io, St> Client<Io, St>
|
||||
where
|
||||
St: 'static,
|
||||
{
|
||||
/// Create new client and provide client id
|
||||
pub fn new(client_id: ByteString) -> Self {
|
||||
Client {
|
||||
client_id,
|
||||
clean_session: true,
|
||||
protocol: mqtt::Protocol::default(),
|
||||
keep_alive: 30,
|
||||
last_will: None,
|
||||
username: None,
|
||||
password: None,
|
||||
inflight: 15,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mqtt protocol version
|
||||
pub fn protocol(mut self, val: mqtt::Protocol) -> Self {
|
||||
self.protocol = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// The handling of the Session state.
|
||||
pub fn clean_session(mut self, val: bool) -> Self {
|
||||
self.clean_session = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// A time interval measured in seconds.
|
||||
///
|
||||
/// keep-alive is set to 30 seconds by default.
|
||||
pub fn keep_alive(mut self, val: u16) -> Self {
|
||||
self.keep_alive = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Will Message be stored on the Server and associated with the Network Connection.
|
||||
///
|
||||
/// by default last will value is not set
|
||||
pub fn last_will(mut self, val: mqtt::LastWill) -> Self {
|
||||
self.last_will = Some(val);
|
||||
self
|
||||
}
|
||||
|
||||
/// Username can be used by the Server for authentication and authorization.
|
||||
pub fn username(mut self, val: ByteString) -> Self {
|
||||
self.username = Some(val);
|
||||
self
|
||||
}
|
||||
|
||||
/// Password can be used by the Server for authentication and authorization.
|
||||
pub fn password(mut self, val: Bytes) -> Self {
|
||||
self.password = Some(val);
|
||||
self
|
||||
}
|
||||
|
||||
/// Number of in-flight concurrent messages.
|
||||
///
|
||||
/// in-flight is set to 15 messages
|
||||
pub fn inflight(mut self, val: usize) -> Self {
|
||||
self.inflight = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set state service
|
||||
///
|
||||
/// State service verifies connect ack packet and construct connection state.
|
||||
pub fn state<C, F>(self, state: F) -> ServiceBuilder<Io, St, C>
|
||||
where
|
||||
F: IntoService<C>,
|
||||
Io: AsyncRead + AsyncWrite,
|
||||
C: Service<Request = ConnectAck<Io>, Response = ConnectAckResult<Io, St>>,
|
||||
C::Error: 'static,
|
||||
{
|
||||
ServiceBuilder {
|
||||
state: Cell::new(state.into_service()),
|
||||
packet: mqtt::Connect {
|
||||
client_id: self.client_id,
|
||||
clean_session: self.clean_session,
|
||||
protocol: self.protocol,
|
||||
keep_alive: self.keep_alive,
|
||||
last_will: self.last_will,
|
||||
username: self.username,
|
||||
password: self.password,
|
||||
},
|
||||
subscribe: Rc::new(boxed::factory(SubsNotImplemented::default())),
|
||||
unsubscribe: Rc::new(boxed::factory(UnsubsNotImplemented::default())),
|
||||
disconnect: None,
|
||||
keep_alive: self.keep_alive.into(),
|
||||
inflight: self.inflight,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServiceBuilder<Io, St, C: Service> {
|
||||
state: Cell<C>,
|
||||
packet: mqtt::Connect,
|
||||
subscribe: Rc<
|
||||
boxed::BoxServiceFactory<
|
||||
St,
|
||||
Subscribe<St>,
|
||||
SubscribeResult,
|
||||
MqttError<C::Error>,
|
||||
MqttError<C::Error>,
|
||||
>,
|
||||
>,
|
||||
unsubscribe: Rc<
|
||||
boxed::BoxServiceFactory<
|
||||
St,
|
||||
Unsubscribe<St>,
|
||||
(),
|
||||
MqttError<C::Error>,
|
||||
MqttError<C::Error>,
|
||||
>,
|
||||
>,
|
||||
disconnect: Option<Cell<boxed::BoxService<St, (), MqttError<C::Error>>>>,
|
||||
keep_alive: u64,
|
||||
inflight: usize,
|
||||
|
||||
_t: PhantomData<(Io, St, C)>,
|
||||
}
|
||||
|
||||
impl<Io, St, C> ServiceBuilder<Io, St, C>
|
||||
where
|
||||
St: Clone + 'static,
|
||||
Io: AsyncRead + AsyncWrite + 'static,
|
||||
C: Service<Request = ConnectAck<Io>, Response = ConnectAckResult<Io, St>> + 'static,
|
||||
C::Error: 'static,
|
||||
{
|
||||
/// Service to execute on disconnect
|
||||
pub fn disconnect<UF, U>(mut self, srv: UF) -> Self
|
||||
where
|
||||
UF: IntoService<U>,
|
||||
U: Service<Request = St, Response = (), Error = C::Error> + 'static,
|
||||
{
|
||||
self.disconnect = Some(Cell::new(boxed::service(
|
||||
srv.into_service().map_err(MqttError::Service),
|
||||
)));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn finish<F, T>(
|
||||
self,
|
||||
service: F,
|
||||
) -> impl Service<Request = Io, Response = (), Error = MqttError<C::Error>>
|
||||
where
|
||||
F: IntoServiceFactory<T>,
|
||||
T: ServiceFactory<
|
||||
Config = St,
|
||||
Request = Publish<St>,
|
||||
Response = (),
|
||||
Error = C::Error,
|
||||
InitError = C::Error,
|
||||
> + 'static,
|
||||
{
|
||||
ioframe::Builder::new()
|
||||
.service(ConnectService {
|
||||
connect: self.state,
|
||||
packet: self.packet,
|
||||
keep_alive: self.keep_alive,
|
||||
inflight: self.inflight,
|
||||
_t: PhantomData,
|
||||
})
|
||||
.finish(dispatcher(
|
||||
service
|
||||
.into_factory()
|
||||
.map_err(MqttError::Service)
|
||||
.map_init_err(MqttError::Service),
|
||||
self.subscribe,
|
||||
self.unsubscribe,
|
||||
))
|
||||
.map_err(|e| match e {
|
||||
ioframe::ServiceError::Service(e) => e,
|
||||
ioframe::ServiceError::Encoder(e) => MqttError::Protocol(e),
|
||||
ioframe::ServiceError::Decoder(e) => MqttError::Protocol(e),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ConnectService<Io, St, C> {
|
||||
connect: Cell<C>,
|
||||
packet: mqtt::Connect,
|
||||
keep_alive: u64,
|
||||
inflight: usize,
|
||||
_t: PhantomData<(Io, St)>,
|
||||
}
|
||||
|
||||
impl<Io, St, C> Service for ConnectService<Io, St, C>
|
||||
where
|
||||
St: 'static,
|
||||
Io: AsyncRead + AsyncWrite + 'static,
|
||||
C: Service<Request = ConnectAck<Io>, Response = ConnectAckResult<Io, St>> + 'static,
|
||||
C::Error: 'static,
|
||||
{
|
||||
type Request = ioframe::Connect<Io, mqtt::Codec>;
|
||||
type Response = ioframe::ConnectResult<Io, MqttState<St>, mqtt::Codec>;
|
||||
type Error = MqttError<C::Error>;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
self.connect
|
||||
.get_mut()
|
||||
.poll_ready(cx)
|
||||
.map_err(MqttError::Service)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Self::Request) -> Self::Future {
|
||||
let mut srv = self.connect.clone();
|
||||
let packet = self.packet.clone();
|
||||
let keep_alive = Duration::from_secs(self.keep_alive as u64);
|
||||
let inflight = self.inflight;
|
||||
|
||||
// send Connect packet
|
||||
async move {
|
||||
let mut framed = req.codec(mqtt::Codec::new());
|
||||
framed
|
||||
.send(mqtt::Packet::Connect(packet))
|
||||
.await
|
||||
.map_err(MqttError::Protocol)?;
|
||||
|
||||
let packet = framed
|
||||
.next()
|
||||
.await
|
||||
.ok_or(MqttError::Disconnected)
|
||||
.and_then(|res| res.map_err(MqttError::Protocol))?;
|
||||
|
||||
match packet {
|
||||
mqtt::Packet::ConnectAck {
|
||||
session_present,
|
||||
return_code,
|
||||
} => {
|
||||
let sink = MqttSink::new(framed.sink().clone());
|
||||
let ack = ConnectAck {
|
||||
sink,
|
||||
session_present,
|
||||
return_code,
|
||||
keep_alive,
|
||||
inflight,
|
||||
io: framed,
|
||||
};
|
||||
Ok(srv
|
||||
.get_mut()
|
||||
.call(ack)
|
||||
.await
|
||||
.map_err(MqttError::Service)
|
||||
.map(|ack| ack.io.state(ack.state))?)
|
||||
}
|
||||
p => Err(MqttError::Unexpected(p, "Expected CONNECT-ACK packet")),
|
||||
}
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConnectAck<Io> {
|
||||
io: ioframe::ConnectResult<Io, (), mqtt::Codec>,
|
||||
sink: MqttSink,
|
||||
session_present: bool,
|
||||
return_code: mqtt::ConnectCode,
|
||||
keep_alive: Duration,
|
||||
inflight: usize,
|
||||
}
|
||||
|
||||
impl<Io> ConnectAck<Io> {
|
||||
#[inline]
|
||||
/// Indicates whether there is already stored Session state
|
||||
pub fn session_present(&self) -> bool {
|
||||
self.session_present
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Connect return code
|
||||
pub fn return_code(&self) -> mqtt::ConnectCode {
|
||||
self.return_code
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Mqtt client sink object
|
||||
pub fn sink(&self) -> &MqttSink {
|
||||
&self.sink
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Set connection state and create result object
|
||||
pub fn state<St>(self, state: St) -> ConnectAckResult<Io, St> {
|
||||
ConnectAckResult {
|
||||
io: self.io,
|
||||
state: MqttState::new(state, self.sink, self.keep_alive, self.inflight),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io> Stream for ConnectAck<Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Item = Result<mqtt::Packet, mqtt::ParseError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
Pin::new(&mut self.io).poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io> Sink<mqtt::Packet> for ConnectAck<Io>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Error = mqtt::ParseError;
|
||||
|
||||
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Pin::new(&mut self.io).poll_ready(cx)
|
||||
}
|
||||
|
||||
fn start_send(mut self: Pin<&mut Self>, item: mqtt::Packet) -> Result<(), Self::Error> {
|
||||
Pin::new(&mut self.io).start_send(item)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Pin::new(&mut self.io).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Pin::new(&mut self.io).poll_close(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project::pin_project]
|
||||
pub struct ConnectAckResult<Io, St> {
|
||||
state: MqttState<St>,
|
||||
io: ioframe::ConnectResult<Io, (), mqtt::Codec>,
|
||||
}
|
||||
|
||||
impl<Io, St> Stream for ConnectAckResult<Io, St>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Item = Result<mqtt::Packet, mqtt::ParseError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
Pin::new(&mut self.io).poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io, St> Sink<mqtt::Packet> for ConnectAckResult<Io, St>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Error = mqtt::ParseError;
|
||||
|
||||
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Pin::new(&mut self.io).poll_ready(cx)
|
||||
}
|
||||
|
||||
fn start_send(mut self: Pin<&mut Self>, item: mqtt::Packet) -> Result<(), Self::Error> {
|
||||
Pin::new(&mut self.io).start_send(item)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Pin::new(&mut self.io).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Pin::new(&mut self.io).poll_close(cx)
|
||||
}
|
||||
}
|
150
actix-mqtt/src/connect.rs
Executable file
150
actix-mqtt/src/connect.rs
Executable file
@@ -0,0 +1,150 @@
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_ioframe as ioframe;
|
||||
use mqtt_codec as mqtt;
|
||||
|
||||
use crate::sink::MqttSink;
|
||||
|
||||
/// Connect message
|
||||
pub struct Connect<Io> {
|
||||
connect: mqtt::Connect,
|
||||
sink: MqttSink,
|
||||
keep_alive: Duration,
|
||||
inflight: usize,
|
||||
io: ioframe::ConnectResult<Io, (), mqtt::Codec>,
|
||||
}
|
||||
|
||||
impl<Io> Connect<Io> {
|
||||
pub(crate) fn new(
|
||||
connect: mqtt::Connect,
|
||||
io: ioframe::ConnectResult<Io, (), mqtt::Codec>,
|
||||
sink: MqttSink,
|
||||
inflight: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
keep_alive: Duration::from_secs(connect.keep_alive as u64),
|
||||
connect,
|
||||
io,
|
||||
sink,
|
||||
inflight,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns reference to io object
|
||||
pub fn get_ref(&self) -> &Io {
|
||||
self.io.get_ref()
|
||||
}
|
||||
|
||||
/// Returns mutable reference to io object
|
||||
pub fn get_mut(&mut self) -> &mut Io {
|
||||
self.io.get_mut()
|
||||
}
|
||||
|
||||
/// Returns mqtt server sink
|
||||
pub fn sink(&self) -> &MqttSink {
|
||||
&self.sink
|
||||
}
|
||||
|
||||
/// Ack connect message and set state
|
||||
pub fn ack<St>(self, st: St, session_present: bool) -> ConnectAck<Io, St> {
|
||||
ConnectAck::new(self.io, st, session_present, self.keep_alive, self.inflight)
|
||||
}
|
||||
|
||||
/// Create connect ack object with `identifier rejected` return code
|
||||
pub fn identifier_rejected<St>(self) -> ConnectAck<Io, St> {
|
||||
ConnectAck {
|
||||
io: self.io,
|
||||
session: None,
|
||||
session_present: false,
|
||||
return_code: mqtt::ConnectCode::IdentifierRejected,
|
||||
keep_alive: Duration::from_secs(5),
|
||||
inflight: 15,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create connect ack object with `bad user name or password` return code
|
||||
pub fn bad_username_or_pwd<St>(self) -> ConnectAck<Io, St> {
|
||||
ConnectAck {
|
||||
io: self.io,
|
||||
session: None,
|
||||
session_present: false,
|
||||
return_code: mqtt::ConnectCode::BadUserNameOrPassword,
|
||||
keep_alive: Duration::from_secs(5),
|
||||
inflight: 15,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create connect ack object with `not authorized` return code
|
||||
pub fn not_authorized<St>(self) -> ConnectAck<Io, St> {
|
||||
ConnectAck {
|
||||
io: self.io,
|
||||
session: None,
|
||||
session_present: false,
|
||||
return_code: mqtt::ConnectCode::NotAuthorized,
|
||||
keep_alive: Duration::from_secs(5),
|
||||
inflight: 15,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io> Deref for Connect<Io> {
|
||||
type Target = mqtt::Connect;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.connect
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for Connect<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.connect.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Ack connect message
|
||||
pub struct ConnectAck<Io, St> {
|
||||
pub(crate) io: ioframe::ConnectResult<Io, (), mqtt::Codec>,
|
||||
pub(crate) session: Option<St>,
|
||||
pub(crate) session_present: bool,
|
||||
pub(crate) return_code: mqtt::ConnectCode,
|
||||
pub(crate) keep_alive: Duration,
|
||||
pub(crate) inflight: usize,
|
||||
}
|
||||
|
||||
impl<Io, St> ConnectAck<Io, St> {
|
||||
/// Create connect ack, `session_present` indicates that previous session is presents
|
||||
pub(crate) fn new(
|
||||
io: ioframe::ConnectResult<Io, (), mqtt::Codec>,
|
||||
session: St,
|
||||
session_present: bool,
|
||||
keep_alive: Duration,
|
||||
inflight: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
io,
|
||||
session_present,
|
||||
keep_alive,
|
||||
inflight,
|
||||
session: Some(session),
|
||||
return_code: mqtt::ConnectCode::ConnectionAccepted,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set idle time-out for the connection in milliseconds
|
||||
///
|
||||
/// By default idle time-out is set to 300000 milliseconds
|
||||
pub fn idle_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.keep_alive = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set in-flight count. Total number of `in-flight` packets
|
||||
///
|
||||
/// By default in-flight count is set to 15
|
||||
pub fn in_flight(mut self, in_flight: usize) -> Self {
|
||||
self.inflight = in_flight;
|
||||
self
|
||||
}
|
||||
}
|
125
actix-mqtt/src/default.rs
Executable file
125
actix-mqtt/src/default.rs
Executable file
@@ -0,0 +1,125 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use futures::future::{ok, Ready};
|
||||
|
||||
use crate::publish::Publish;
|
||||
use crate::subs::{Subscribe, SubscribeResult, Unsubscribe};
|
||||
|
||||
/// Not implemented publish service
|
||||
pub struct NotImplemented<S, E>(PhantomData<(S, E)>);
|
||||
|
||||
impl<S, E> Default for NotImplemented<S, E> {
|
||||
fn default() -> Self {
|
||||
NotImplemented(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> ServiceFactory for NotImplemented<S, E> {
|
||||
type Config = S;
|
||||
type Request = Publish<S>;
|
||||
type Response = ();
|
||||
type Error = E;
|
||||
type InitError = E;
|
||||
type Service = NotImplemented<S, E>;
|
||||
type Future = Ready<Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: S) -> Self::Future {
|
||||
ok(NotImplemented(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> Service for NotImplemented<S, E> {
|
||||
type Request = Publish<S>;
|
||||
type Response = ();
|
||||
type Error = E;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, _: Publish<S>) -> Self::Future {
|
||||
log::warn!("MQTT Publish is not implemented");
|
||||
ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Not implemented subscribe service
|
||||
pub struct SubsNotImplemented<S, E>(PhantomData<(S, E)>);
|
||||
|
||||
impl<S, E> Default for SubsNotImplemented<S, E> {
|
||||
fn default() -> Self {
|
||||
SubsNotImplemented(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> ServiceFactory for SubsNotImplemented<S, E> {
|
||||
type Config = S;
|
||||
type Request = Subscribe<S>;
|
||||
type Response = SubscribeResult;
|
||||
type Error = E;
|
||||
type InitError = E;
|
||||
type Service = SubsNotImplemented<S, E>;
|
||||
type Future = Ready<Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: S) -> Self::Future {
|
||||
ok(SubsNotImplemented(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> Service for SubsNotImplemented<S, E> {
|
||||
type Request = Subscribe<S>;
|
||||
type Response = SubscribeResult;
|
||||
type Error = E;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, subs: Subscribe<S>) -> Self::Future {
|
||||
log::warn!("MQTT Subscribe is not implemented");
|
||||
ok(subs.into_result())
|
||||
}
|
||||
}
|
||||
|
||||
/// Not implemented unsubscribe service
|
||||
pub struct UnsubsNotImplemented<S, E>(PhantomData<(S, E)>);
|
||||
|
||||
impl<S, E> Default for UnsubsNotImplemented<S, E> {
|
||||
fn default() -> Self {
|
||||
UnsubsNotImplemented(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> ServiceFactory for UnsubsNotImplemented<S, E> {
|
||||
type Config = S;
|
||||
type Request = Unsubscribe<S>;
|
||||
type Response = ();
|
||||
type Error = E;
|
||||
type InitError = E;
|
||||
type Service = UnsubsNotImplemented<S, E>;
|
||||
type Future = Ready<Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: S) -> Self::Future {
|
||||
ok(UnsubsNotImplemented(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> Service for UnsubsNotImplemented<S, E> {
|
||||
type Request = Unsubscribe<S>;
|
||||
type Response = ();
|
||||
type Error = E;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, _: Unsubscribe<S>) -> Self::Future {
|
||||
log::warn!("MQTT Unsubscribe is not implemented");
|
||||
ok(())
|
||||
}
|
||||
}
|
286
actix-mqtt/src/dispatcher.rs
Executable file
286
actix-mqtt/src/dispatcher.rs
Executable file
@@ -0,0 +1,286 @@
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::num::NonZeroU16;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_ioframe as ioframe;
|
||||
use actix_service::{boxed, fn_factory_with_config, pipeline, Service, ServiceFactory};
|
||||
use actix_utils::inflight::InFlightService;
|
||||
use actix_utils::keepalive::KeepAliveService;
|
||||
use actix_utils::order::{InOrder, InOrderError};
|
||||
use actix_utils::time::LowResTimeService;
|
||||
use futures::future::{join3, ok, Either, FutureExt, LocalBoxFuture, Ready};
|
||||
use futures::ready;
|
||||
use mqtt_codec as mqtt;
|
||||
|
||||
use crate::cell::Cell;
|
||||
use crate::error::MqttError;
|
||||
use crate::publish::Publish;
|
||||
use crate::sink::MqttSink;
|
||||
use crate::subs::{Subscribe, SubscribeResult, Unsubscribe};
|
||||
|
||||
pub(crate) struct MqttState<St> {
|
||||
inner: Cell<MqttStateInner<St>>,
|
||||
}
|
||||
|
||||
struct MqttStateInner<St> {
|
||||
pub(crate) st: St,
|
||||
pub(crate) sink: MqttSink,
|
||||
pub(self) timeout: Duration,
|
||||
pub(self) in_flight: usize,
|
||||
}
|
||||
|
||||
impl<St> Clone for MqttState<St> {
|
||||
fn clone(&self) -> Self {
|
||||
MqttState {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<St> MqttState<St> {
|
||||
pub(crate) fn new(st: St, sink: MqttSink, timeout: Duration, in_flight: usize) -> Self {
|
||||
MqttState {
|
||||
inner: Cell::new(MqttStateInner {
|
||||
st,
|
||||
sink,
|
||||
timeout,
|
||||
in_flight,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn sink(&self) -> &MqttSink {
|
||||
&self.inner.sink
|
||||
}
|
||||
|
||||
pub(crate) fn session(&self) -> &St {
|
||||
&self.inner.get_ref().st
|
||||
}
|
||||
|
||||
pub(crate) fn session_mut(&mut self) -> &mut St {
|
||||
&mut self.inner.get_mut().st
|
||||
}
|
||||
}
|
||||
|
||||
// dispatcher factory
|
||||
pub(crate) fn dispatcher<St, T, E>(
|
||||
publish: T,
|
||||
subscribe: Rc<
|
||||
boxed::BoxServiceFactory<
|
||||
St,
|
||||
Subscribe<St>,
|
||||
SubscribeResult,
|
||||
MqttError<E>,
|
||||
MqttError<E>,
|
||||
>,
|
||||
>,
|
||||
unsubscribe: Rc<
|
||||
boxed::BoxServiceFactory<St, Unsubscribe<St>, (), MqttError<E>, MqttError<E>>,
|
||||
>,
|
||||
) -> impl ServiceFactory<
|
||||
Config = MqttState<St>,
|
||||
Request = ioframe::Item<MqttState<St>, mqtt::Codec>,
|
||||
Response = Option<mqtt::Packet>,
|
||||
Error = MqttError<E>,
|
||||
InitError = MqttError<E>,
|
||||
>
|
||||
where
|
||||
E: 'static,
|
||||
St: Clone + 'static,
|
||||
T: ServiceFactory<
|
||||
Config = St,
|
||||
Request = Publish<St>,
|
||||
Response = (),
|
||||
Error = MqttError<E>,
|
||||
InitError = MqttError<E>,
|
||||
> + 'static,
|
||||
{
|
||||
let time = LowResTimeService::with(Duration::from_secs(1));
|
||||
|
||||
fn_factory_with_config(move |cfg: MqttState<St>| {
|
||||
let time = time.clone();
|
||||
let state = cfg.session().clone();
|
||||
let timeout = cfg.inner.timeout;
|
||||
let inflight = cfg.inner.in_flight;
|
||||
|
||||
// create services
|
||||
let fut = join3(
|
||||
publish.new_service(state.clone()),
|
||||
subscribe.new_service(state.clone()),
|
||||
unsubscribe.new_service(state.clone()),
|
||||
);
|
||||
|
||||
async move {
|
||||
let (publish, subscribe, unsubscribe) = fut.await;
|
||||
|
||||
// mqtt dispatcher
|
||||
Ok(Dispatcher::new(
|
||||
// keep-alive connection
|
||||
pipeline(KeepAliveService::new(timeout, time, || {
|
||||
MqttError::KeepAliveTimeout
|
||||
}))
|
||||
.and_then(
|
||||
// limit number of in-flight messages
|
||||
InFlightService::new(
|
||||
inflight,
|
||||
// mqtt spec requires ack ordering, so enforce response ordering
|
||||
InOrder::service(publish?).map_err(|e| match e {
|
||||
InOrderError::Service(e) => e,
|
||||
InOrderError::Disconnected => MqttError::Disconnected,
|
||||
}),
|
||||
),
|
||||
),
|
||||
subscribe?,
|
||||
unsubscribe?,
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// PUBLIS/SUBSCRIBER/UNSUBSCRIBER packets dispatcher
|
||||
pub(crate) struct Dispatcher<St, T: Service> {
|
||||
publish: T,
|
||||
subscribe: boxed::BoxService<Subscribe<St>, SubscribeResult, T::Error>,
|
||||
unsubscribe: boxed::BoxService<Unsubscribe<St>, (), T::Error>,
|
||||
}
|
||||
|
||||
impl<St, T> Dispatcher<St, T>
|
||||
where
|
||||
T: Service<Request = Publish<St>, Response = ()>,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
publish: T,
|
||||
subscribe: boxed::BoxService<Subscribe<St>, SubscribeResult, T::Error>,
|
||||
unsubscribe: boxed::BoxService<Unsubscribe<St>, (), T::Error>,
|
||||
) -> Self {
|
||||
Self {
|
||||
publish,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<St, T> Service for Dispatcher<St, T>
|
||||
where
|
||||
T: Service<Request = Publish<St>, Response = ()>,
|
||||
T::Error: 'static,
|
||||
{
|
||||
type Request = ioframe::Item<MqttState<St>, mqtt::Codec>;
|
||||
type Response = Option<mqtt::Packet>;
|
||||
type Error = T::Error;
|
||||
type Future = Either<
|
||||
Either<
|
||||
Ready<Result<Self::Response, T::Error>>,
|
||||
LocalBoxFuture<'static, Result<Self::Response, T::Error>>,
|
||||
>,
|
||||
PublishResponse<T::Future, T::Error>,
|
||||
>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
let res1 = self.publish.poll_ready(cx)?;
|
||||
let res2 = self.subscribe.poll_ready(cx)?;
|
||||
let res3 = self.unsubscribe.poll_ready(cx)?;
|
||||
|
||||
if res1.is_pending() || res2.is_pending() || res3.is_pending() {
|
||||
Poll::Pending
|
||||
} else {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
fn call(&mut self, req: ioframe::Item<MqttState<St>, mqtt::Codec>) -> Self::Future {
|
||||
let (mut state, _, packet) = req.into_parts();
|
||||
|
||||
log::trace!("Dispatch packet: {:#?}", packet);
|
||||
match packet {
|
||||
mqtt::Packet::PingRequest => {
|
||||
Either::Left(Either::Left(ok(Some(mqtt::Packet::PingResponse))))
|
||||
}
|
||||
mqtt::Packet::Disconnect => Either::Left(Either::Left(ok(None))),
|
||||
mqtt::Packet::Publish(publish) => {
|
||||
let packet_id = publish.packet_id;
|
||||
Either::Right(PublishResponse {
|
||||
packet_id,
|
||||
fut: self.publish.call(Publish::new(state, publish)),
|
||||
_t: PhantomData,
|
||||
})
|
||||
}
|
||||
mqtt::Packet::PublishAck { packet_id } => {
|
||||
state.inner.get_mut().sink.complete_publish_qos1(packet_id);
|
||||
Either::Left(Either::Left(ok(None)))
|
||||
}
|
||||
mqtt::Packet::Subscribe {
|
||||
packet_id,
|
||||
topic_filters,
|
||||
} => Either::Left(Either::Right(
|
||||
SubscribeResponse {
|
||||
packet_id,
|
||||
fut: self.subscribe.call(Subscribe::new(state, topic_filters)),
|
||||
}
|
||||
.boxed_local(),
|
||||
)),
|
||||
mqtt::Packet::Unsubscribe {
|
||||
packet_id,
|
||||
topic_filters,
|
||||
} => Either::Left(Either::Right(
|
||||
self.unsubscribe
|
||||
.call(Unsubscribe::new(state, topic_filters))
|
||||
.map(move |_| Ok(Some(mqtt::Packet::UnsubscribeAck { packet_id })))
|
||||
.boxed_local(),
|
||||
)),
|
||||
_ => Either::Left(Either::Left(ok(None))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish service response future
|
||||
#[pin_project::pin_project]
|
||||
pub(crate) struct PublishResponse<T, E> {
|
||||
#[pin]
|
||||
fut: T,
|
||||
packet_id: Option<NonZeroU16>,
|
||||
_t: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<T, E> Future for PublishResponse<T, E>
|
||||
where
|
||||
T: Future<Output = Result<(), E>>,
|
||||
{
|
||||
type Output = Result<Option<mqtt::Packet>, E>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
|
||||
ready!(this.fut.poll(cx))?;
|
||||
if let Some(packet_id) = this.packet_id {
|
||||
Poll::Ready(Ok(Some(mqtt::Packet::PublishAck {
|
||||
packet_id: *packet_id,
|
||||
})))
|
||||
} else {
|
||||
Poll::Ready(Ok(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe service response future
|
||||
pub(crate) struct SubscribeResponse<E> {
|
||||
fut: LocalBoxFuture<'static, Result<SubscribeResult, E>>,
|
||||
packet_id: NonZeroU16,
|
||||
}
|
||||
|
||||
impl<E> Future for SubscribeResponse<E> {
|
||||
type Output = Result<Option<mqtt::Packet>, E>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let res = ready!(Pin::new(&mut self.fut).poll(cx))?;
|
||||
Poll::Ready(Ok(Some(mqtt::Packet::SubscribeAck {
|
||||
status: res.codes,
|
||||
packet_id: self.packet_id,
|
||||
})))
|
||||
}
|
||||
}
|
34
actix-mqtt/src/error.rs
Executable file
34
actix-mqtt/src/error.rs
Executable file
@@ -0,0 +1,34 @@
|
||||
use std::io;
|
||||
|
||||
/// Errors which can occur when attempting to handle mqtt connection.
|
||||
#[derive(Debug)]
|
||||
pub enum MqttError<E> {
|
||||
/// Message handler service error
|
||||
Service(E),
|
||||
/// Mqtt parse error
|
||||
Protocol(mqtt_codec::ParseError),
|
||||
/// Unexpected packet
|
||||
Unexpected(mqtt_codec::Packet, &'static str),
|
||||
/// "SUBSCRIBE, UNSUBSCRIBE, and PUBLISH (in cases where QoS > 0) Control Packets MUST contain a non-zero 16-bit Packet Identifier [MQTT-2.3.1-1]."
|
||||
PacketIdRequired,
|
||||
/// Keep alive timeout
|
||||
KeepAliveTimeout,
|
||||
/// Handshake timeout
|
||||
HandshakeTimeout,
|
||||
/// Peer disconnect
|
||||
Disconnected,
|
||||
/// Unexpected io error
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl<E> From<mqtt_codec::ParseError> for MqttError<E> {
|
||||
fn from(err: mqtt_codec::ParseError) -> Self {
|
||||
MqttError::Protocol(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<io::Error> for MqttError<E> {
|
||||
fn from(err: io::Error) -> Self {
|
||||
MqttError::Io(err)
|
||||
}
|
||||
}
|
23
actix-mqtt/src/lib.rs
Executable file
23
actix-mqtt/src/lib.rs
Executable file
@@ -0,0 +1,23 @@
|
||||
#![allow(clippy::type_complexity, clippy::new_ret_no_self)]
|
||||
//! MQTT v3.1 Server framework
|
||||
|
||||
mod cell;
|
||||
pub mod client;
|
||||
mod connect;
|
||||
mod default;
|
||||
mod dispatcher;
|
||||
mod error;
|
||||
mod publish;
|
||||
mod router;
|
||||
mod server;
|
||||
mod sink;
|
||||
mod subs;
|
||||
|
||||
pub use self::client::Client;
|
||||
pub use self::connect::{Connect, ConnectAck};
|
||||
pub use self::error::MqttError;
|
||||
pub use self::publish::Publish;
|
||||
pub use self::router::Router;
|
||||
pub use self::server::MqttServer;
|
||||
pub use self::sink::MqttSink;
|
||||
pub use self::subs::{Subscribe, SubscribeIter, SubscribeResult, Subscription, Unsubscribe};
|
137
actix-mqtt/src/publish.rs
Executable file
137
actix-mqtt/src/publish.rs
Executable file
@@ -0,0 +1,137 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::num::NonZeroU16;
|
||||
|
||||
use actix_router::Path;
|
||||
use bytes::Bytes;
|
||||
use bytestring::ByteString;
|
||||
use mqtt_codec as mqtt;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Error as JsonError;
|
||||
|
||||
use crate::dispatcher::MqttState;
|
||||
use crate::sink::MqttSink;
|
||||
|
||||
/// Publish message
|
||||
pub struct Publish<S> {
|
||||
publish: mqtt::Publish,
|
||||
sink: MqttSink,
|
||||
state: MqttState<S>,
|
||||
topic: Path<ByteString>,
|
||||
query: Option<ByteString>,
|
||||
}
|
||||
|
||||
impl<S> Publish<S> {
|
||||
pub(crate) fn new(state: MqttState<S>, publish: mqtt::Publish) -> Self {
|
||||
let (topic, query) = if let Some(pos) = publish.topic.find('?') {
|
||||
(
|
||||
ByteString::try_from(publish.topic.get_ref().slice(0..pos)).unwrap(),
|
||||
Some(
|
||||
ByteString::try_from(
|
||||
publish.topic.get_ref().slice(pos + 1..publish.topic.len()),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(publish.topic.clone(), None)
|
||||
};
|
||||
let topic = Path::new(topic);
|
||||
let sink = state.sink().clone();
|
||||
Self {
|
||||
sink,
|
||||
publish,
|
||||
state,
|
||||
topic,
|
||||
query,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// this might be re-delivery of an earlier attempt to send the Packet.
|
||||
pub fn dup(&self) -> bool {
|
||||
self.publish.dup
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn retain(&self) -> bool {
|
||||
self.publish.retain
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// the level of assurance for delivery of an Application Message.
|
||||
pub fn qos(&self) -> mqtt::QoS {
|
||||
self.publish.qos
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// the information channel to which payload data is published.
|
||||
pub fn publish_topic(&self) -> &str {
|
||||
&self.publish.topic
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// returns reference to a connection session
|
||||
pub fn session(&self) -> &S {
|
||||
self.state.session()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// returns mutable reference to a connection session
|
||||
pub fn session_mut(&mut self) -> &mut S {
|
||||
self.state.session_mut()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// only present in PUBLISH Packets where the QoS level is 1 or 2.
|
||||
pub fn id(&self) -> Option<NonZeroU16> {
|
||||
self.publish.packet_id
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn topic(&self) -> &Path<ByteString> {
|
||||
&self.topic
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn topic_mut(&mut self) -> &mut Path<ByteString> {
|
||||
&mut self.topic
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn query(&self) -> &str {
|
||||
self.query.as_ref().map(|s| s.as_ref()).unwrap_or("")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn packet(&self) -> &mqtt::Publish {
|
||||
&self.publish
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// the Application Message that is being published.
|
||||
pub fn payload(&self) -> &Bytes {
|
||||
&self.publish.payload
|
||||
}
|
||||
|
||||
/// Extract Bytes from packet payload
|
||||
pub fn take_payload(&self) -> Bytes {
|
||||
self.publish.payload.clone()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Mqtt client sink object
|
||||
pub fn sink(&self) -> &MqttSink {
|
||||
&self.sink
|
||||
}
|
||||
|
||||
/// Loads and parse `application/json` encoded body.
|
||||
pub fn json<T: DeserializeOwned>(&mut self) -> Result<T, JsonError> {
|
||||
serde_json::from_slice(&self.publish.payload)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> std::fmt::Debug for Publish<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
self.publish.fmt(f)
|
||||
}
|
||||
}
|
206
actix-mqtt/src/router.rs
Executable file
206
actix-mqtt/src/router.rs
Executable file
@@ -0,0 +1,206 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use actix_router::{IntoPattern, RouterBuilder};
|
||||
use actix_service::boxed::{self, BoxService, BoxServiceFactory};
|
||||
use actix_service::{fn_service, IntoServiceFactory, Service, ServiceFactory};
|
||||
use futures::future::{join_all, ok, JoinAll, LocalBoxFuture};
|
||||
|
||||
use crate::publish::Publish;
|
||||
|
||||
type Handler<S, E> = BoxServiceFactory<S, Publish<S>, (), E, E>;
|
||||
type HandlerService<S, E> = BoxService<Publish<S>, (), E>;
|
||||
|
||||
/// Router - structure that follows the builder pattern
|
||||
/// for building publish packet router instances for mqtt server.
|
||||
pub struct Router<S, E> {
|
||||
router: RouterBuilder<usize>,
|
||||
handlers: Vec<Handler<S, E>>,
|
||||
default: Handler<S, E>,
|
||||
}
|
||||
|
||||
impl<S, E> Router<S, E>
|
||||
where
|
||||
S: Clone + 'static,
|
||||
E: 'static,
|
||||
{
|
||||
/// Create mqtt application.
|
||||
///
|
||||
/// **Note** Default service acks all publish packets
|
||||
pub fn new() -> Self {
|
||||
Router {
|
||||
router: actix_router::Router::build(),
|
||||
handlers: Vec::new(),
|
||||
default: boxed::factory(
|
||||
fn_service(|p: Publish<S>| {
|
||||
log::warn!("Unknown topic {:?}", p.publish_topic());
|
||||
ok::<_, E>(())
|
||||
})
|
||||
.map_init_err(|_| panic!()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure mqtt resource for a specific topic.
|
||||
pub fn resource<T, F, U: 'static>(mut self, address: T, service: F) -> Self
|
||||
where
|
||||
T: IntoPattern,
|
||||
F: IntoServiceFactory<U>,
|
||||
U: ServiceFactory<Config = S, Request = Publish<S>, Response = (), Error = E>,
|
||||
E: From<U::InitError>,
|
||||
{
|
||||
self.router.path(address, self.handlers.len());
|
||||
self.handlers
|
||||
.push(boxed::factory(service.into_factory().map_init_err(E::from)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Default service to be used if no matching resource could be found.
|
||||
pub fn default_resource<F, U: 'static>(mut self, service: F) -> Self
|
||||
where
|
||||
F: IntoServiceFactory<U>,
|
||||
U: ServiceFactory<
|
||||
Config = S,
|
||||
Request = Publish<S>,
|
||||
Response = (),
|
||||
Error = E,
|
||||
InitError = E,
|
||||
>,
|
||||
{
|
||||
self.default = boxed::factory(service.into_factory());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> IntoServiceFactory<RouterFactory<S, E>> for Router<S, E>
|
||||
where
|
||||
S: Clone + 'static,
|
||||
E: 'static,
|
||||
{
|
||||
fn into_factory(self) -> RouterFactory<S, E> {
|
||||
RouterFactory {
|
||||
router: Rc::new(self.router.finish()),
|
||||
handlers: self.handlers,
|
||||
default: self.default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RouterFactory<S, E> {
|
||||
router: Rc<actix_router::Router<usize>>,
|
||||
handlers: Vec<Handler<S, E>>,
|
||||
default: Handler<S, E>,
|
||||
}
|
||||
|
||||
impl<S, E> ServiceFactory for RouterFactory<S, E>
|
||||
where
|
||||
S: Clone + 'static,
|
||||
E: 'static,
|
||||
{
|
||||
type Config = S;
|
||||
type Request = Publish<S>;
|
||||
type Response = ();
|
||||
type Error = E;
|
||||
type InitError = E;
|
||||
type Service = RouterService<S, E>;
|
||||
type Future = RouterFactoryFut<S, E>;
|
||||
|
||||
fn new_service(&self, session: S) -> Self::Future {
|
||||
let fut: Vec<_> = self
|
||||
.handlers
|
||||
.iter()
|
||||
.map(|h| h.new_service(session.clone()))
|
||||
.collect();
|
||||
|
||||
RouterFactoryFut {
|
||||
router: self.router.clone(),
|
||||
handlers: join_all(fut),
|
||||
default: Some(either::Either::Left(self.default.new_service(session))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RouterFactoryFut<S, E> {
|
||||
router: Rc<actix_router::Router<usize>>,
|
||||
handlers: JoinAll<LocalBoxFuture<'static, Result<HandlerService<S, E>, E>>>,
|
||||
default: Option<
|
||||
either::Either<
|
||||
LocalBoxFuture<'static, Result<HandlerService<S, E>, E>>,
|
||||
HandlerService<S, E>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl<S, E> Future for RouterFactoryFut<S, E> {
|
||||
type Output = Result<RouterService<S, E>, E>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let res = match self.default.as_mut().unwrap() {
|
||||
either::Either::Left(ref mut fut) => {
|
||||
let default = match futures::ready!(Pin::new(fut).poll(cx)) {
|
||||
Ok(default) => default,
|
||||
Err(e) => return Poll::Ready(Err(e)),
|
||||
};
|
||||
self.default = Some(either::Either::Right(default));
|
||||
return self.poll(cx);
|
||||
}
|
||||
either::Either::Right(_) => futures::ready!(Pin::new(&mut self.handlers).poll(cx)),
|
||||
};
|
||||
|
||||
let mut handlers = Vec::new();
|
||||
for handler in res {
|
||||
match handler {
|
||||
Ok(h) => handlers.push(h),
|
||||
Err(e) => return Poll::Ready(Err(e)),
|
||||
}
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(RouterService {
|
||||
handlers,
|
||||
router: self.router.clone(),
|
||||
default: self.default.take().unwrap().right().unwrap(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RouterService<S, E> {
|
||||
router: Rc<actix_router::Router<usize>>,
|
||||
handlers: Vec<BoxService<Publish<S>, (), E>>,
|
||||
default: BoxService<Publish<S>, (), E>,
|
||||
}
|
||||
|
||||
impl<S, E> Service for RouterService<S, E>
|
||||
where
|
||||
S: 'static,
|
||||
E: 'static,
|
||||
{
|
||||
type Request = Publish<S>;
|
||||
type Response = ();
|
||||
type Error = E;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
let mut not_ready = false;
|
||||
for hnd in &mut self.handlers {
|
||||
if let Poll::Pending = hnd.poll_ready(cx)? {
|
||||
not_ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
if not_ready {
|
||||
Poll::Pending
|
||||
} else {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
fn call(&mut self, mut req: Publish<S>) -> Self::Future {
|
||||
if let Some((idx, _info)) = self.router.recognize(req.topic_mut()) {
|
||||
self.handlers[*idx].call(req)
|
||||
} else {
|
||||
self.default.call(req)
|
||||
}
|
||||
}
|
||||
}
|
331
actix-mqtt/src/server.rs
Executable file
331
actix-mqtt/src/server.rs
Executable file
@@ -0,0 +1,331 @@
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_ioframe as ioframe;
|
||||
use actix_service::{apply, apply_fn, boxed, fn_factory, pipeline_factory, unit_config};
|
||||
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
||||
use actix_utils::timeout::{Timeout, TimeoutError};
|
||||
use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use mqtt_codec as mqtt;
|
||||
|
||||
use crate::cell::Cell;
|
||||
use crate::connect::{Connect, ConnectAck};
|
||||
use crate::default::{SubsNotImplemented, UnsubsNotImplemented};
|
||||
use crate::dispatcher::{dispatcher, MqttState};
|
||||
use crate::error::MqttError;
|
||||
use crate::publish::Publish;
|
||||
use crate::sink::MqttSink;
|
||||
use crate::subs::{Subscribe, SubscribeResult, Unsubscribe};
|
||||
|
||||
/// Mqtt Server
|
||||
pub struct MqttServer<Io, St, C: ServiceFactory, U> {
|
||||
connect: C,
|
||||
subscribe: boxed::BoxServiceFactory<
|
||||
St,
|
||||
Subscribe<St>,
|
||||
SubscribeResult,
|
||||
MqttError<C::Error>,
|
||||
MqttError<C::Error>,
|
||||
>,
|
||||
unsubscribe: boxed::BoxServiceFactory<
|
||||
St,
|
||||
Unsubscribe<St>,
|
||||
(),
|
||||
MqttError<C::Error>,
|
||||
MqttError<C::Error>,
|
||||
>,
|
||||
disconnect: U,
|
||||
max_size: usize,
|
||||
inflight: usize,
|
||||
handshake_timeout: u64,
|
||||
_t: PhantomData<(Io, St)>,
|
||||
}
|
||||
|
||||
fn default_disconnect<St>(_: St, _: bool) {}
|
||||
|
||||
impl<Io, St, C> MqttServer<Io, St, C, ()>
|
||||
where
|
||||
St: 'static,
|
||||
C: ServiceFactory<Config = (), Request = Connect<Io>, Response = ConnectAck<Io, St>>
|
||||
+ 'static,
|
||||
{
|
||||
/// Create server factory and provide connect service
|
||||
pub fn new<F>(connect: F) -> MqttServer<Io, St, C, impl Fn(St, bool)>
|
||||
where
|
||||
F: IntoServiceFactory<C>,
|
||||
{
|
||||
MqttServer {
|
||||
connect: connect.into_factory(),
|
||||
subscribe: boxed::factory(
|
||||
pipeline_factory(SubsNotImplemented::default())
|
||||
.map_err(MqttError::Service)
|
||||
.map_init_err(MqttError::Service),
|
||||
),
|
||||
unsubscribe: boxed::factory(
|
||||
pipeline_factory(UnsubsNotImplemented::default())
|
||||
.map_err(MqttError::Service)
|
||||
.map_init_err(MqttError::Service),
|
||||
),
|
||||
max_size: 0,
|
||||
inflight: 15,
|
||||
disconnect: default_disconnect,
|
||||
handshake_timeout: 0,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Io, St, C, U> MqttServer<Io, St, C, U>
|
||||
where
|
||||
St: Clone + 'static,
|
||||
U: Fn(St, bool) + 'static,
|
||||
C: ServiceFactory<Config = (), Request = Connect<Io>, Response = ConnectAck<Io, St>>
|
||||
+ 'static,
|
||||
{
|
||||
/// Set handshake timeout in millis.
|
||||
///
|
||||
/// Handshake includes `connect` packet and response `connect-ack`.
|
||||
/// By default handshake timeuot is disabled.
|
||||
pub fn handshake_timeout(mut self, timeout: u64) -> Self {
|
||||
self.handshake_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set max inbound frame size.
|
||||
///
|
||||
/// If max size is set to `0`, size is unlimited.
|
||||
/// By default max size is set to `0`
|
||||
pub fn max_size(mut self, size: usize) -> Self {
|
||||
self.max_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Number of in-flight concurrent messages.
|
||||
///
|
||||
/// in-flight is set to 15 messages
|
||||
pub fn inflight(mut self, val: usize) -> Self {
|
||||
self.inflight = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Service to execute for subscribe packet
|
||||
pub fn subscribe<F, Srv>(mut self, subscribe: F) -> Self
|
||||
where
|
||||
F: IntoServiceFactory<Srv>,
|
||||
Srv: ServiceFactory<Config = St, Request = Subscribe<St>, Response = SubscribeResult>
|
||||
+ 'static,
|
||||
C::Error: From<Srv::Error> + From<Srv::InitError>,
|
||||
{
|
||||
self.subscribe = boxed::factory(
|
||||
subscribe
|
||||
.into_factory()
|
||||
.map_err(|e| MqttError::Service(e.into()))
|
||||
.map_init_err(|e| MqttError::Service(e.into())),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Service to execute for unsubscribe packet
|
||||
pub fn unsubscribe<F, Srv>(mut self, unsubscribe: F) -> Self
|
||||
where
|
||||
F: IntoServiceFactory<Srv>,
|
||||
Srv: ServiceFactory<Config = St, Request = Unsubscribe<St>, Response = ()> + 'static,
|
||||
C::Error: From<Srv::Error> + From<Srv::InitError>,
|
||||
{
|
||||
self.unsubscribe = boxed::factory(
|
||||
unsubscribe
|
||||
.into_factory()
|
||||
.map_err(|e| MqttError::Service(e.into()))
|
||||
.map_init_err(|e| MqttError::Service(e.into())),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Callback to execute on disconnect
|
||||
///
|
||||
/// Second parameter indicates error occured during disconnect.
|
||||
pub fn disconnect<F, Out>(self, disconnect: F) -> MqttServer<Io, St, C, impl Fn(St, bool)>
|
||||
where
|
||||
F: Fn(St, bool) -> Out,
|
||||
Out: Future + 'static,
|
||||
{
|
||||
MqttServer {
|
||||
connect: self.connect,
|
||||
subscribe: self.subscribe,
|
||||
unsubscribe: self.unsubscribe,
|
||||
max_size: self.max_size,
|
||||
inflight: self.inflight,
|
||||
handshake_timeout: self.handshake_timeout,
|
||||
disconnect: move |st: St, err| {
|
||||
let fut = disconnect(st, err);
|
||||
actix_rt::spawn(fut.map(|_| ()));
|
||||
},
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set service to execute for publish packet and create service factory
|
||||
pub fn finish<F, P>(
|
||||
self,
|
||||
publish: F,
|
||||
) -> impl ServiceFactory<Config = (), Request = Io, Response = (), Error = MqttError<C::Error>>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + 'static,
|
||||
F: IntoServiceFactory<P>,
|
||||
P: ServiceFactory<Config = St, Request = Publish<St>, Response = ()> + 'static,
|
||||
C::Error: From<P::Error> + From<P::InitError>,
|
||||
{
|
||||
let connect = self.connect;
|
||||
let max_size = self.max_size;
|
||||
let handshake_timeout = self.handshake_timeout;
|
||||
let disconnect = self.disconnect;
|
||||
let publish = boxed::factory(
|
||||
publish
|
||||
.into_factory()
|
||||
.map_err(|e| MqttError::Service(e.into()))
|
||||
.map_init_err(|e| MqttError::Service(e.into())),
|
||||
);
|
||||
|
||||
unit_config(
|
||||
ioframe::Builder::new()
|
||||
.factory(connect_service_factory(
|
||||
connect,
|
||||
max_size,
|
||||
self.inflight,
|
||||
handshake_timeout,
|
||||
))
|
||||
.disconnect(move |cfg, err| disconnect(cfg.session().clone(), err))
|
||||
.finish(dispatcher(
|
||||
publish,
|
||||
Rc::new(self.subscribe),
|
||||
Rc::new(self.unsubscribe),
|
||||
))
|
||||
.map_err(|e| match e {
|
||||
ioframe::ServiceError::Service(e) => e,
|
||||
ioframe::ServiceError::Encoder(e) => MqttError::Protocol(e),
|
||||
ioframe::ServiceError::Decoder(e) => MqttError::Protocol(e),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn connect_service_factory<Io, St, C>(
|
||||
factory: C,
|
||||
max_size: usize,
|
||||
inflight: usize,
|
||||
handshake_timeout: u64,
|
||||
) -> impl ServiceFactory<
|
||||
Config = (),
|
||||
Request = ioframe::Connect<Io, mqtt::Codec>,
|
||||
Response = ioframe::ConnectResult<Io, MqttState<St>, mqtt::Codec>,
|
||||
Error = MqttError<C::Error>,
|
||||
>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite,
|
||||
C: ServiceFactory<Config = (), Request = Connect<Io>, Response = ConnectAck<Io, St>>,
|
||||
{
|
||||
apply(
|
||||
Timeout::new(Duration::from_millis(handshake_timeout)),
|
||||
fn_factory(move || {
|
||||
let fut = factory.new_service(());
|
||||
|
||||
async move {
|
||||
let service = Cell::new(fut.await?);
|
||||
|
||||
Ok::<_, C::InitError>(apply_fn(
|
||||
service.map_err(MqttError::Service),
|
||||
move |conn: ioframe::Connect<Io, mqtt::Codec>, service| {
|
||||
let mut srv = service.clone();
|
||||
let mut framed = conn.codec(mqtt::Codec::new().max_size(max_size));
|
||||
|
||||
async move {
|
||||
// read first packet
|
||||
let packet = framed
|
||||
.next()
|
||||
.await
|
||||
.ok_or(MqttError::Disconnected)
|
||||
.and_then(|res| res.map_err(|e| MqttError::Protocol(e)))?;
|
||||
|
||||
match packet {
|
||||
mqtt::Packet::Connect(connect) => {
|
||||
let sink = MqttSink::new(framed.sink().clone());
|
||||
|
||||
// authenticate mqtt connection
|
||||
let mut ack = srv
|
||||
.call(Connect::new(
|
||||
connect,
|
||||
framed,
|
||||
sink.clone(),
|
||||
inflight,
|
||||
))
|
||||
.await?;
|
||||
|
||||
match ack.session {
|
||||
Some(session) => {
|
||||
log::trace!(
|
||||
"Sending: {:#?}",
|
||||
mqtt::Packet::ConnectAck {
|
||||
session_present: ack.session_present,
|
||||
return_code:
|
||||
mqtt::ConnectCode::ConnectionAccepted,
|
||||
}
|
||||
);
|
||||
ack.io
|
||||
.send(mqtt::Packet::ConnectAck {
|
||||
session_present: ack.session_present,
|
||||
return_code:
|
||||
mqtt::ConnectCode::ConnectionAccepted,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(ack.io.state(MqttState::new(
|
||||
session,
|
||||
sink,
|
||||
ack.keep_alive,
|
||||
ack.inflight,
|
||||
)))
|
||||
}
|
||||
None => {
|
||||
log::trace!(
|
||||
"Sending: {:#?}",
|
||||
mqtt::Packet::ConnectAck {
|
||||
session_present: false,
|
||||
return_code: ack.return_code,
|
||||
}
|
||||
);
|
||||
|
||||
ack.io
|
||||
.send(mqtt::Packet::ConnectAck {
|
||||
session_present: false,
|
||||
return_code: ack.return_code,
|
||||
})
|
||||
.await?;
|
||||
Err(MqttError::Disconnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
packet => {
|
||||
log::info!(
|
||||
"MQTT-3.1.0-1: Expected CONNECT packet, received {}",
|
||||
packet.packet_type()
|
||||
);
|
||||
Err(MqttError::Unexpected(
|
||||
packet,
|
||||
"MQTT-3.1.0-1: Expected CONNECT packet",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.map_err(|e| match e {
|
||||
TimeoutError::Service(e) => e,
|
||||
TimeoutError::Timeout => MqttError::HandshakeTimeout,
|
||||
})
|
||||
}
|
107
actix-mqtt/src/sink.rs
Executable file
107
actix-mqtt/src/sink.rs
Executable file
@@ -0,0 +1,107 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt;
|
||||
use std::num::NonZeroU16;
|
||||
|
||||
use actix_ioframe::Sink;
|
||||
use actix_utils::oneshot;
|
||||
use bytes::Bytes;
|
||||
use bytestring::ByteString;
|
||||
use futures::future::{Future, TryFutureExt};
|
||||
use mqtt_codec as mqtt;
|
||||
|
||||
use crate::cell::Cell;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MqttSink {
|
||||
sink: Sink<mqtt::Packet>,
|
||||
pub(crate) inner: Cell<MqttSinkInner>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct MqttSinkInner {
|
||||
pub(crate) idx: u16,
|
||||
pub(crate) queue: VecDeque<(u16, oneshot::Sender<()>)>,
|
||||
}
|
||||
|
||||
impl MqttSink {
|
||||
pub(crate) fn new(sink: Sink<mqtt::Packet>) -> Self {
|
||||
MqttSink {
|
||||
sink,
|
||||
inner: Cell::new(MqttSinkInner::default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Close mqtt connection
|
||||
pub fn close(&self) {
|
||||
self.sink.close();
|
||||
}
|
||||
|
||||
/// Send publish packet with qos set to 0
|
||||
pub fn publish_qos0(&self, topic: ByteString, payload: Bytes, dup: bool) {
|
||||
log::trace!("Publish (QoS0) to {:?}", topic);
|
||||
let publish = mqtt::Publish {
|
||||
topic,
|
||||
payload,
|
||||
dup,
|
||||
retain: false,
|
||||
qos: mqtt::QoS::AtMostOnce,
|
||||
packet_id: None,
|
||||
};
|
||||
self.sink.send(mqtt::Packet::Publish(publish));
|
||||
}
|
||||
|
||||
/// Send publish packet
|
||||
pub fn publish_qos1(
|
||||
&mut self,
|
||||
topic: ByteString,
|
||||
payload: Bytes,
|
||||
dup: bool,
|
||||
) -> impl Future<Output = Result<(), ()>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let inner = self.inner.get_mut();
|
||||
inner.idx += 1;
|
||||
if inner.idx == 0 {
|
||||
inner.idx = 1
|
||||
}
|
||||
inner.queue.push_back((inner.idx, tx));
|
||||
|
||||
let publish = mqtt::Packet::Publish(mqtt::Publish {
|
||||
topic,
|
||||
payload,
|
||||
dup,
|
||||
retain: false,
|
||||
qos: mqtt::QoS::AtLeastOnce,
|
||||
packet_id: NonZeroU16::new(inner.idx),
|
||||
});
|
||||
log::trace!("Publish (QoS1) to {:#?}", publish);
|
||||
|
||||
self.sink.send(publish);
|
||||
rx.map_err(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn complete_publish_qos1(&mut self, packet_id: NonZeroU16) {
|
||||
if let Some((idx, tx)) = self.inner.get_mut().queue.pop_front() {
|
||||
if idx != packet_id.get() {
|
||||
log::trace!(
|
||||
"MQTT protocol error, packet_id order does not match, expected {}, got: {}",
|
||||
idx,
|
||||
packet_id
|
||||
);
|
||||
self.close();
|
||||
} else {
|
||||
log::trace!("Ack publish packet with id: {}", packet_id);
|
||||
let _ = tx.send(());
|
||||
}
|
||||
} else {
|
||||
log::trace!("Unexpected PublishAck packet");
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for MqttSink {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.debug_struct("MqttSink").finish()
|
||||
}
|
||||
}
|
191
actix-mqtt/src/subs.rs
Executable file
191
actix-mqtt/src/subs.rs
Executable file
@@ -0,0 +1,191 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use bytestring::ByteString;
|
||||
use mqtt_codec as mqtt;
|
||||
|
||||
use crate::dispatcher::MqttState;
|
||||
use crate::sink::MqttSink;
|
||||
|
||||
/// Subscribe message
|
||||
pub struct Subscribe<S> {
|
||||
topics: Vec<(ByteString, mqtt::QoS)>,
|
||||
codes: Vec<mqtt::SubscribeReturnCode>,
|
||||
state: MqttState<S>,
|
||||
}
|
||||
|
||||
/// Result of a subscribe message
|
||||
pub struct SubscribeResult {
|
||||
pub(crate) codes: Vec<mqtt::SubscribeReturnCode>,
|
||||
}
|
||||
|
||||
impl<S> Subscribe<S> {
|
||||
pub(crate) fn new(state: MqttState<S>, topics: Vec<(ByteString, mqtt::QoS)>) -> Self {
|
||||
let mut codes = Vec::with_capacity(topics.len());
|
||||
(0..topics.len()).for_each(|_| codes.push(mqtt::SubscribeReturnCode::Failure));
|
||||
|
||||
Self {
|
||||
topics,
|
||||
state,
|
||||
codes,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// returns reference to a connection session
|
||||
pub fn session(&self) -> &S {
|
||||
self.state.session()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// returns mutable reference to a connection session
|
||||
pub fn session_mut(&mut self) -> &mut S {
|
||||
self.state.session_mut()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Mqtt client sink object
|
||||
pub fn sink(&self) -> MqttSink {
|
||||
self.state.sink().clone()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// returns iterator over subscription topics
|
||||
pub fn iter_mut(&mut self) -> SubscribeIter<S> {
|
||||
SubscribeIter {
|
||||
subs: self as *const _ as *mut _,
|
||||
entry: 0,
|
||||
lt: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// convert subscription to a result
|
||||
pub fn into_result(self) -> SubscribeResult {
|
||||
SubscribeResult { codes: self.codes }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S> IntoIterator for &'a mut Subscribe<S> {
|
||||
type Item = Subscription<'a, S>;
|
||||
type IntoIter = SubscribeIter<'a, S>;
|
||||
|
||||
fn into_iter(self) -> SubscribeIter<'a, S> {
|
||||
self.iter_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over subscription topics
|
||||
pub struct SubscribeIter<'a, S> {
|
||||
subs: *mut Subscribe<S>,
|
||||
entry: usize,
|
||||
lt: PhantomData<&'a mut Subscribe<S>>,
|
||||
}
|
||||
|
||||
impl<'a, S> SubscribeIter<'a, S> {
|
||||
fn next_unsafe(&mut self) -> Option<Subscription<'a, S>> {
|
||||
let subs = unsafe { &mut *self.subs };
|
||||
|
||||
if self.entry < subs.topics.len() {
|
||||
let s = Subscription {
|
||||
topic: &subs.topics[self.entry].0,
|
||||
qos: subs.topics[self.entry].1,
|
||||
state: subs.state.clone(),
|
||||
code: &mut subs.codes[self.entry],
|
||||
};
|
||||
self.entry += 1;
|
||||
Some(s)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S> Iterator for SubscribeIter<'a, S> {
|
||||
type Item = Subscription<'a, S>;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Subscription<'a, S>> {
|
||||
self.next_unsafe()
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscription topic
|
||||
pub struct Subscription<'a, S> {
|
||||
topic: &'a ByteString,
|
||||
state: MqttState<S>,
|
||||
qos: mqtt::QoS,
|
||||
code: &'a mut mqtt::SubscribeReturnCode,
|
||||
}
|
||||
|
||||
impl<'a, S> Subscription<'a, S> {
|
||||
#[inline]
|
||||
/// reference to a connection session
|
||||
pub fn session(&self) -> &S {
|
||||
self.state.session()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// mutable reference to a connection session
|
||||
pub fn session_mut(&mut self) -> &mut S {
|
||||
self.state.session_mut()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// subscription topic
|
||||
pub fn topic(&self) -> &'a ByteString {
|
||||
&self.topic
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// the level of assurance for delivery of an Application Message.
|
||||
pub fn qos(&self) -> mqtt::QoS {
|
||||
self.qos
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// fail to subscribe to the topic
|
||||
pub fn fail(&mut self) {
|
||||
*self.code = mqtt::SubscribeReturnCode::Failure
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// subscribe to a topic with specific qos
|
||||
pub fn subscribe(&mut self, qos: mqtt::QoS) {
|
||||
*self.code = mqtt::SubscribeReturnCode::Success(qos)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unsubscribe message
|
||||
pub struct Unsubscribe<S> {
|
||||
state: MqttState<S>,
|
||||
topics: Vec<ByteString>,
|
||||
}
|
||||
|
||||
impl<S> Unsubscribe<S> {
|
||||
pub(crate) fn new(state: MqttState<S>, topics: Vec<ByteString>) -> Self {
|
||||
Self { topics, state }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// reference to a connection session
|
||||
pub fn session(&self) -> &S {
|
||||
self.state.session()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// mutable reference to a connection session
|
||||
pub fn session_mut(&mut self) -> &mut S {
|
||||
self.state.session_mut()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Mqtt client sink object
|
||||
pub fn sink(&self) -> MqttSink {
|
||||
self.state.sink().clone()
|
||||
}
|
||||
|
||||
/// returns iterator over unsubscribe topics
|
||||
pub fn iter(&self) -> impl Iterator<Item = &ByteString> {
|
||||
self.topics.iter()
|
||||
}
|
||||
}
|
52
actix-mqtt/tests/test_server.rs
Executable file
52
actix-mqtt/tests/test_server.rs
Executable file
@@ -0,0 +1,52 @@
|
||||
use actix_service::Service;
|
||||
use actix_testing::TestServer;
|
||||
use bytes::Bytes;
|
||||
use bytestring::ByteString;
|
||||
use futures::future::ok;
|
||||
|
||||
use actix_mqtt::{client, Connect, ConnectAck, MqttServer, Publish};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Session;
|
||||
|
||||
async fn connect<Io>(packet: Connect<Io>) -> Result<ConnectAck<Io, Session>, ()> {
|
||||
println!("CONNECT: {:?}", packet);
|
||||
Ok(packet.ack(Session, false))
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_simple() -> std::io::Result<()> {
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
"actix_codec=info,actix_server=trace,actix_connector=trace",
|
||||
);
|
||||
env_logger::init();
|
||||
|
||||
let srv = TestServer::with(|| MqttServer::new(connect).finish(|_t| ok(())));
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ClientSession;
|
||||
|
||||
let mut client = client::Client::new(ByteString::from_static("user"))
|
||||
.state(|ack: client::ConnectAck<_>| async move {
|
||||
ack.sink()
|
||||
.publish_qos0(ByteString::from_static("#"), Bytes::new(), false);
|
||||
ack.sink().close();
|
||||
Ok(ack.state(ClientSession))
|
||||
})
|
||||
.finish(|_t: Publish<_>| {
|
||||
async {
|
||||
// t.sink().close();
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
let conn = actix_connect::default_connector()
|
||||
.call(actix_connect::Connect::with(String::new(), srv.addr()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client.call(conn.into_parts().0).await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
Reference in New Issue
Block a user