├── .github └── workflows │ ├── build.yml │ ├── projects.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE ├── README.md ├── clippy.toml ├── examples ├── multiple-things.rs └── single-thing.rs ├── src ├── action.rs ├── action_generator.rs ├── event.rs ├── lib.rs ├── property.rs ├── server.rs ├── thing.rs └── utils.rs └── test.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | rust-version: [ 17 | 'stable', 18 | 'beta', 19 | 'nightly', 20 | ] 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: actions/setup-python@v2 24 | with: 25 | python-version: '3.9' 26 | - name: Install dependencies 27 | run: | 28 | rustup install ${{ matrix.rust-version }} 29 | rustup default ${{ matrix.rust-version }} 30 | - name: Run integration tests 31 | run: | 32 | ./test.sh 33 | -------------------------------------------------------------------------------- /.github/workflows/projects.yml: -------------------------------------------------------------------------------- 1 | name: Add new issues to the specified project column 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | add-new-issues-to-project-column: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: add-new-issues-to-organization-based-project-column 12 | uses: docker://takanabe/github-actions-automate-projects:v0.0.1 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.CI_TOKEN }} 15 | GITHUB_PROJECT_URL: https://github.com/orgs/WebThingsIO/projects/4 16 | GITHUB_PROJECT_COLUMN_NAME: To do 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Install dependencies 14 | run: | 15 | rustup install stable 16 | rustup default stable 17 | - name: Set release version 18 | run: echo "RELEASE_VERSION=${GITHUB_REF:11}" >> $GITHUB_ENV 19 | - name: Create Release 20 | id: create_release 21 | uses: actions/create-release@v1.0.0 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | tag_name: ${{ github.ref }} 26 | release_name: Release ${{ env.RELEASE_VERSION }} 27 | draft: false 28 | prerelease: false 29 | - name: Publish to crates.io 30 | run: | 31 | cargo publish --token ${{ secrets.CARGO_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | /target 13 | **/*.rs.bk 14 | 15 | /target 16 | **/*.rs.bk 17 | Cargo.lock 18 | 19 | *.swp 20 | 21 | /webthing-tester/ 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # webthing Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.15.0] - 2022-03-07 6 | ### Added 7 | - BaseActionGenerator to reduce boilerplate when creating a server which doesn't need actions 8 | ### Changed 9 | - BaseThing and the Thing trait now support richer contexts 10 | - Move to actix web 4 11 | 12 | ## [0.14.0] - 2021-01-05 13 | ### Added 14 | - Parameter to disable host validation in server. 15 | 16 | ## [0.13.2] - 2020-12-29 17 | ### Changed 18 | - Added libmdns::Service property called `dns_service` to WebThingServer 19 | - Rust example publishes mDNS-SD (service discovery) correctly 20 | 21 | ## [0.13.1] - 2020-09-23 22 | ### Changed 23 | - Update author and URLs to indicate new project home. 24 | 25 | ## [0.13.0] - 2020-09-16 26 | ### Changed 27 | - Now using 2018 edition of Rust. 28 | - Updated to modern version of actix-web. 29 | - Server is now created and started with `.start()`; `.create()` is no longer present. 30 | - Server no longer takes an optional router. Instead, an optional `configure` closure can be passed to `.start()`, which will be used via `App.configure()`. 31 | 32 | ## [0.12.4] - 2020-06-18 33 | ### Changed 34 | - mDNS record now indicates TLS support. 35 | 36 | ## [0.12.3] - 2020-05-04 37 | ### Changed 38 | - Invalid POST requests to action resources now generate an error status. 39 | 40 | ## [0.12.2] - 2020-03-27 41 | ### Changed 42 | - Updated dependencies. 43 | 44 | ## [0.12.1] - 2019-11-07 45 | ### Changed 46 | - Fixed deprecations. 47 | 48 | ## [0.12.0] - 2019-07-12 49 | ### Changed 50 | - Things now use `title` rather than `name`. 51 | - Things now require a unique ID in the form of a URI. 52 | ### Added 53 | - Ability to set a base URL path on server. 54 | - Support for `id`, `base`, `security`, and `securityDefinitions` keys in thing description. 55 | 56 | ## [0.11.0] - 2019-01-16 57 | ### Changed 58 | - `WebThingServer::new()` can now take a configuration function which can add additional API routes. 59 | ### Fixed 60 | - Properties could not include a custom `links` array at initialization. 61 | 62 | ## [0.10.3] - 2018-12-18 63 | ### Fixed 64 | - SSL feature compilation. 65 | 66 | ## [0.10.2] - 2018-12-18 67 | ### Changed 68 | - SSL is now an optional feature. 69 | 70 | ## [0.10.1] - 2018-12-13 71 | ### Changed 72 | - Properties, actions, and events should now use `title` rather than `label`. 73 | 74 | ## [0.10.0] - 2018-11-30 75 | ### Changed 76 | - Property, Action, and Event description now use `links` rather than `href`. - [Spec PR](https://github.com/WebThingsIO/wot/pull/119) 77 | 78 | [Unreleased]: https://github.com/WebThingsIO/webthing-rust/compare/v0.15.0...HEAD 79 | [0.15.0]: https://github.com/WebThingsIO/webthing-rust/compare/v0.14.0...v0.15.0 80 | [0.14.0]: https://github.com/WebThingsIO/webthing-rust/compare/v0.13.2...v0.14.0 81 | [0.13.2]: https://github.com/WebThingsIO/webthing-rust/compare/v0.13.1...v0.13.2 82 | [0.13.1]: https://github.com/WebThingsIO/webthing-rust/compare/v0.13.0...v0.13.1 83 | [0.13.0]: https://github.com/WebThingsIO/webthing-rust/compare/v0.12.4...v0.13.0 84 | [0.12.4]: https://github.com/WebThingsIO/webthing-rust/compare/v0.12.3...v0.12.4 85 | [0.12.3]: https://github.com/WebThingsIO/webthing-rust/compare/v0.12.2...v0.12.3 86 | [0.12.2]: https://github.com/WebThingsIO/webthing-rust/compare/v0.12.1...v0.12.2 87 | [0.12.1]: https://github.com/WebThingsIO/webthing-rust/compare/v0.12.0...v0.12.1 88 | [0.12.0]: https://github.com/WebThingsIO/webthing-rust/compare/v0.11.0...v0.12.0 89 | [0.11.0]: https://github.com/WebThingsIO/webthing-rust/compare/v0.10.3...v0.11.0 90 | [0.10.3]: https://github.com/WebThingsIO/webthing-rust/compare/v0.10.2...v0.10.3 91 | [0.10.2]: https://github.com/WebThingsIO/webthing-rust/compare/v0.10.1...v0.10.2 92 | [0.10.1]: https://github.com/WebThingsIO/webthing-rust/compare/v0.10.0...v0.10.1 93 | [0.10.0]: https://github.com/WebThingsIO/webthing-rust/compare/v0.9.3...v0.10.0 94 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webthing" 3 | version = "0.15.2" 4 | authors = ["WebThingsIO "] 5 | repository = "https://github.com/WebThingsIO/webthing-rust" 6 | homepage = "https://github.com/WebThingsIO/webthing-rust" 7 | license = "MPL-2.0" 8 | readme = "README.md" 9 | description = "Implementation of an HTTP Web Thing." 10 | edition = "2021" 11 | 12 | [dependencies] 13 | actix = { version = "0.13", optional = true } 14 | actix-web = { version = "4.0.0", optional = true } 15 | actix-web-actors = { version = "4.0.0", optional = true } 16 | chrono = { version = "0.4.22", default_features = false, features = ["std"] } 17 | futures = { version = "0.3", optional = true } 18 | hostname = { version = "0.3", optional = true } 19 | if-addrs = { version = "0.7", optional = true } 20 | libmdns = { version = "0.7", optional = true } 21 | openssl = { version = "0.10", optional = true } 22 | serde_json = "1.0" 23 | uuid = { version = "1.0", features = ["v4"] } 24 | valico = "3.5" 25 | 26 | [dev-dependencies] 27 | actix-rt = "2.6" 28 | env_logger = "0.9" 29 | rand = "0.8" 30 | 31 | [features] 32 | default = ["actix"] 33 | actix = [ 34 | "dep:actix", 35 | "actix-web", 36 | "actix-web-actors", 37 | "futures", 38 | "if-addrs", 39 | "hostname", 40 | "libmdns", 41 | ] 42 | ssl = ["actix", "actix-web/openssl", "openssl"] 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webthing 2 | 3 | [![Build Status](https://github.com/WebThingsIO/webthing-rust/workflows/Rust%20package/badge.svg)](https://github.com/WebThingsIO/webthing-rust/workflows/Rust%20package) 4 | [![Crates.io](https://img.shields.io/crates/v/webthing.svg)](https://crates.io/crates/webthing) 5 | [![license](https://img.shields.io/badge/license-MPL--2.0-blue.svg)](LICENSE) 6 | 7 | Implementation of an HTTP [Web Thing](https://iot.mozilla.org/wot/). 8 | 9 | # Using 10 | 11 | If you're using `Cargo`, just add `webthing` to your `Cargo.toml`: 12 | 13 | ```toml 14 | [dependencies] 15 | webthing = "0.15" 16 | ``` 17 | 18 | ## TLS Support 19 | 20 | If you need TLS support for the server, you'll need to compile with the `ssl` feature set. 21 | 22 | # Example 23 | 24 | In this example we will set up a dimmable light and a humidity sensor (both using fake data, of course). Both working examples can be found in [here](https://github.com/WebThingsIO/webthing-rust/tree/master/examples). 25 | 26 | ## Dimmable Light 27 | 28 | Imagine you have a dimmable light that you want to expose via the web of things API. The light can be turned on/off and the brightness can be set from 0% to 100%. Besides the name, description, and type, a [`Light`](https://iot.mozilla.org/schemas/#Light) is required to expose two properties: 29 | * `on`: the state of the light, whether it is turned on or off 30 | * Setting this property via a `PUT {"on": true/false}` call to the REST API toggles the light. 31 | * `brightness`: the brightness level of the light from 0-100% 32 | * Setting this property via a PUT call to the REST API sets the brightness level of this light. 33 | 34 | First we create a new Thing: 35 | 36 | ```rust 37 | let mut light = BaseThing::new( 38 | "urn:dev:ops:my-lamp-1234".to_owned(), 39 | "My Lamp".to_owned(), 40 | Some(vec!["OnOffSwitch".to_owned(), "Light".to_owned()]), 41 | Some("A web connected lamp".to_owned()), 42 | ); 43 | ``` 44 | 45 | Now we can add the required properties. 46 | 47 | The **`on`** property reports and sets the on/off state of the light. For our purposes, we just want to log the new state if the light is switched on/off. 48 | 49 | ```rust 50 | struct OnValueForwarder; 51 | 52 | impl ValueForwarder for OnValueForwarder { 53 | fn set_value(&mut self, value: serde_json::Value) -> Result { 54 | println!("On-State is now {}", value); 55 | Ok(value) 56 | } 57 | } 58 | 59 | let on_description = json!({ 60 | "@type": "OnProperty", 61 | "title": "On/Off", 62 | "type": "boolean", 63 | "description": "Whether the lamp is turned on" 64 | }); 65 | let on_description = on_description.as_object().unwrap().clone(); 66 | thing.add_property(Box::new(BaseProperty::new( 67 | "on".to_owned(), 68 | json!(true), 69 | Some(Box::new(OnValueForwarder)), 70 | Some(on_description), 71 | ))); 72 | ``` 73 | 74 | The **`brightness`** property reports the brightness level of the light and sets the level. Like before, instead of actually setting the level of a light, we just log the level. 75 | 76 | ```rust 77 | struct BrightnessValueForwarder; 78 | 79 | impl ValueForwarder for BrightnessValueForwarder { 80 | fn set_value(&mut self, value: serde_json::Value) -> Result { 81 | println!("Brightness is now {}", value); 82 | Ok(value) 83 | } 84 | } 85 | 86 | let brightness_description = json!({ 87 | "@type": "BrightnessProperty", 88 | "title": "Brightness", 89 | "type": "number", 90 | "description": "The level of light from 0-100", 91 | "minimum": 0, 92 | "maximum": 100, 93 | "unit": "percent" 94 | }); 95 | let brightness_description = brightness_description.as_object().unwrap().clone(); 96 | thing.add_property(Box::new(BaseProperty::new( 97 | "brightness".to_owned(), 98 | json!(50), 99 | Some(Box::new(BrightnessValueForwarder)), 100 | Some(brightness_description), 101 | ))); 102 | ``` 103 | 104 | Now we can add our newly created thing to the server and start it: 105 | 106 | ```rust 107 | let mut things: Vec>>> = Vec::new(); 108 | things.push(Arc::new(RwLock::new(Box::new(light))); 109 | 110 | // If adding more than one thing, use ThingsType::Multiple() with a name. 111 | // In the single thing case, the thing's name will be broadcast. 112 | let mut server = WebThingServer::new( 113 | ThingsType::Multiple(things, "LightAndTempDevice".to_owned()), 114 | Some(8888), 115 | None, 116 | None, 117 | Box::new(Generator), 118 | None, 119 | None, 120 | ); 121 | let server_addr = server.create(); 122 | server.start(); 123 | ``` 124 | 125 | This will start the server, making the light available via the WoT REST API and announcing it as a discoverable resource on your local network via mDNS. 126 | 127 | ## Sensor 128 | 129 | Let's now also connect a humidity sensor to the server we set up for our light. 130 | 131 | A [`MultiLevelSensor`](https://iot.mozilla.org/schemas/#MultiLevelSensor) (a sensor that returns a level instead of just on/off) has one required property (besides the name, type, and optional description): **`level`**. We want to monitor this property and get notified if the value changes. 132 | 133 | First we create a new Thing: 134 | 135 | ```rust 136 | let mut thing = BaseThing::new( 137 | "urn:dev:ops:my-humidity-sensor-1234".to_owned(), 138 | "My Humidity Sensor".to_owned(), 139 | Some(vec!["MultiLevelSensor".to_owned()]), 140 | Some("A web connected humidity sensor".to_owned()), 141 | ); 142 | ``` 143 | 144 | Then we create and add the appropriate property: 145 | * `level`: tells us what the sensor is actually reading 146 | * Contrary to the light, the value cannot be set via an API call, as it wouldn't make much sense, to SET what a sensor is reading. Therefore, we are creating a *readOnly* property. 147 | 148 | ```rust 149 | let level_description = json!({ 150 | "@type": "LevelProperty", 151 | "title": "Humidity", 152 | "type": "number", 153 | "description": "The current humidity in %", 154 | "minimum": 0, 155 | "maximum": 100, 156 | "unit": "percent", 157 | "readOnly": true 158 | }); 159 | let level_description = level_description.as_object().unwrap().clone(); 160 | thing.add_property(Box::new(BaseProperty::new( 161 | "level".to_owned(), 162 | json!(0), 163 | None, 164 | Some(level_description), 165 | ))); 166 | ``` 167 | 168 | Now we have a sensor that constantly reports 0%. To make it usable, we need a thread or some kind of input when the sensor has a new reading available. For this purpose we start a thread that queries the physical sensor every few seconds. For our purposes, it just calls a fake method. 169 | 170 | ```rust 171 | let sensor = Arc::new(RwLock::new(Box::new(sensor)))); 172 | let cloned = sensor.clone(); 173 | thread::spawn(move || { 174 | let mut rng = rand::thread_rng(); 175 | 176 | // Mimic an actual sensor updating its reading every couple seconds. 177 | loop { 178 | thread::sleep(time::Duration::from_millis(3000)); 179 | let t = cloned.clone(); 180 | let new_value = json!( 181 | 70.0 * rng.gen_range::(0.0, 1.0) * (-0.5 + rng.gen_range::(0.0, 1.0)) 182 | ); 183 | 184 | { 185 | let mut t = t.write().unwrap(); 186 | let prop = t.find_property("level".to_owned()).unwrap(); 187 | let _ = prop.set_value(new_value.clone()); 188 | } 189 | 190 | t.write() 191 | .unwrap() 192 | .property_notify("level".to_owned(), new_value); 193 | } 194 | }); 195 | ``` 196 | 197 | This will update our property with random sensor readings. The new property value is then sent to all websocket listeners. 198 | 199 | # Adding to Gateway 200 | 201 | To add your web thing to the WebThings Gateway, install the "Web Thing" add-on and follow the instructions [here](https://github.com/WebThingsIO/thing-url-adapter#readme). 202 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | type-complexity-threshold = 300 2 | -------------------------------------------------------------------------------- /examples/multiple-things.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | use serde_json::json; 3 | use std::sync::{Arc, RwLock, Weak}; 4 | use std::{thread, time}; 5 | use uuid::Uuid; 6 | use webthing::property::ValueForwarder; 7 | use webthing::server::ActionGenerator; 8 | use webthing::{ 9 | Action, BaseAction, BaseEvent, BaseProperty, BaseThing, Thing, ThingsType, WebThingServer, 10 | }; 11 | 12 | pub struct FadeAction(BaseAction); 13 | 14 | impl FadeAction { 15 | fn new( 16 | input: Option>, 17 | thing: Weak>>, 18 | ) -> FadeAction { 19 | FadeAction(BaseAction::new( 20 | Uuid::new_v4().to_string(), 21 | "fade".to_owned(), 22 | input, 23 | thing, 24 | )) 25 | } 26 | } 27 | 28 | impl Action for FadeAction { 29 | fn set_href_prefix(&mut self, prefix: String) { 30 | self.0.set_href_prefix(prefix) 31 | } 32 | 33 | fn get_id(&self) -> String { 34 | self.0.get_id() 35 | } 36 | 37 | fn get_name(&self) -> String { 38 | self.0.get_name() 39 | } 40 | 41 | fn get_href(&self) -> String { 42 | self.0.get_href() 43 | } 44 | 45 | fn get_status(&self) -> String { 46 | self.0.get_status() 47 | } 48 | 49 | fn get_time_requested(&self) -> String { 50 | self.0.get_time_requested() 51 | } 52 | 53 | fn get_time_completed(&self) -> Option { 54 | self.0.get_time_completed() 55 | } 56 | 57 | fn get_input(&self) -> Option> { 58 | self.0.get_input() 59 | } 60 | 61 | fn get_thing(&self) -> Option>>> { 62 | self.0.get_thing() 63 | } 64 | 65 | fn set_status(&mut self, status: String) { 66 | self.0.set_status(status) 67 | } 68 | 69 | fn start(&mut self) { 70 | self.0.start() 71 | } 72 | 73 | fn perform_action(&mut self) { 74 | let thing = self.get_thing(); 75 | if thing.is_none() { 76 | return; 77 | } 78 | 79 | let thing = thing.unwrap(); 80 | let input = self.get_input().unwrap(); 81 | let name = self.get_name(); 82 | let id = self.get_id(); 83 | 84 | thread::spawn(move || { 85 | thread::sleep(time::Duration::from_millis( 86 | input.get("duration").unwrap().as_u64().unwrap(), 87 | )); 88 | 89 | let thing = thing.clone(); 90 | let mut thing = thing.write().unwrap(); 91 | let _ = thing.set_property( 92 | "brightness".to_owned(), 93 | input.get("brightness").unwrap().clone(), 94 | ); 95 | thing.add_event(Box::new(BaseEvent::new( 96 | "overheated".to_owned(), 97 | Some(json!(102)), 98 | ))); 99 | 100 | thing.finish_action(name, id); 101 | }); 102 | } 103 | 104 | fn cancel(&mut self) { 105 | self.0.cancel() 106 | } 107 | 108 | fn finish(&mut self) { 109 | self.0.finish() 110 | } 111 | } 112 | 113 | struct Generator; 114 | 115 | impl ActionGenerator for Generator { 116 | fn generate( 117 | &self, 118 | thing: Weak>>, 119 | name: String, 120 | input: Option<&serde_json::Value>, 121 | ) -> Option> { 122 | let input = input.and_then(|v| v.as_object()).cloned(); 123 | let name: &str = &name; 124 | match name { 125 | "fade" => Some(Box::new(FadeAction::new(input, thing))), 126 | _ => None, 127 | } 128 | } 129 | } 130 | 131 | struct OnValueForwarder; 132 | 133 | impl ValueForwarder for OnValueForwarder { 134 | fn set_value(&mut self, value: serde_json::Value) -> Result { 135 | println!("On-State is now {}", value); 136 | Ok(value) 137 | } 138 | } 139 | 140 | struct BrightnessValueForwarder; 141 | 142 | impl ValueForwarder for BrightnessValueForwarder { 143 | fn set_value(&mut self, value: serde_json::Value) -> Result { 144 | println!("Brightness is now {}", value); 145 | Ok(value) 146 | } 147 | } 148 | 149 | /// A dimmable light that logs received commands to stdout. 150 | fn make_light() -> Arc>> { 151 | let mut thing = BaseThing::new( 152 | "urn:dev:ops:my-lamp-1234".to_owned(), 153 | "My Lamp".to_owned(), 154 | Some(vec!["OnOffSwitch".to_owned(), "Light".to_owned()]), 155 | Some("A web connected lamp".to_owned()), 156 | ); 157 | 158 | let on_description = json!({ 159 | "@type": "OnOffProperty", 160 | "title": "On/Off", 161 | "type": "boolean", 162 | "description": "Whether the lamp is turned on" 163 | }); 164 | let on_description = on_description.as_object().unwrap().clone(); 165 | thing.add_property(Box::new(BaseProperty::new( 166 | "on".to_owned(), 167 | json!(true), 168 | Some(Box::new(OnValueForwarder)), 169 | Some(on_description), 170 | ))); 171 | 172 | let brightness_description = json!({ 173 | "@type": "BrightnessProperty", 174 | "title": "Brightness", 175 | "type": "integer", 176 | "description": "The level of light from 0-100", 177 | "minimum": 0, 178 | "maximum": 100, 179 | "unit": "percent" 180 | }); 181 | let brightness_description = brightness_description.as_object().unwrap().clone(); 182 | thing.add_property(Box::new(BaseProperty::new( 183 | "brightness".to_owned(), 184 | json!(50), 185 | Some(Box::new(BrightnessValueForwarder)), 186 | Some(brightness_description), 187 | ))); 188 | 189 | let fade_metadata = json!({ 190 | "title": "Fade", 191 | "description": "Fade the lamp to a given level", 192 | "input": { 193 | "type": "object", 194 | "required": [ 195 | "brightness", 196 | "duration" 197 | ], 198 | "properties": { 199 | "brightness": { 200 | "type": "integer", 201 | "minimum": 0, 202 | "maximum": 100, 203 | "unit": "percent" 204 | }, 205 | "duration": { 206 | "type": "integer", 207 | "minimum": 1, 208 | "unit": "milliseconds" 209 | } 210 | } 211 | } 212 | }); 213 | let fade_metadata = fade_metadata.as_object().unwrap().clone(); 214 | thing.add_available_action("fade".to_owned(), fade_metadata); 215 | 216 | let overheated_metadata = json!({ 217 | "description": "The lamp has exceeded its safe operating temperature", 218 | "type": "number", 219 | "unit": "degree celsius" 220 | }); 221 | let overheated_metadata = overheated_metadata.as_object().unwrap().clone(); 222 | thing.add_available_event("overheated".to_owned(), overheated_metadata); 223 | 224 | Arc::new(RwLock::new(Box::new(thing))) 225 | } 226 | 227 | /// A humidity sensor which updates its measurement every few seconds. 228 | fn make_sensor() -> Arc>> { 229 | let mut thing = BaseThing::new( 230 | "urn:dev:ops:my-humidity-sensor-1234".to_owned(), 231 | "My Humidity Sensor".to_owned(), 232 | Some(vec!["MultiLevelSensor".to_owned()]), 233 | Some("A web connected humidity sensor".to_owned()), 234 | ); 235 | 236 | let level_description = json!({ 237 | "@type": "LevelProperty", 238 | "title": "Humidity", 239 | "type": "number", 240 | "description": "The current humidity in %", 241 | "minimum": 0, 242 | "maximum": 100, 243 | "unit": "percent", 244 | "readOnly": true 245 | }); 246 | let level_description = level_description.as_object().unwrap().clone(); 247 | thing.add_property(Box::new(BaseProperty::new( 248 | "level".to_owned(), 249 | json!(0), 250 | None, 251 | Some(level_description), 252 | ))); 253 | 254 | Arc::new(RwLock::new(Box::new(thing))) 255 | } 256 | 257 | #[actix_rt::main] 258 | async fn main() -> std::io::Result<()> { 259 | env_logger::init(); 260 | 261 | let mut things: Vec>>> = Vec::new(); 262 | 263 | // Create a thing that represents a dimmable light 264 | things.push(make_light()); 265 | 266 | // Create a thing that represents a humidity sensor 267 | let sensor = make_sensor(); 268 | things.push(sensor.clone()); 269 | 270 | let cloned = sensor.clone(); 271 | thread::spawn(move || { 272 | let mut rng = rand::thread_rng(); 273 | 274 | // Mimic an actual sensor updating its reading every couple seconds. 275 | loop { 276 | thread::sleep(time::Duration::from_millis(3000)); 277 | let t = cloned.clone(); 278 | let new_value: f32 = 70.0 * rng.gen_range(0.0..1.0) * (-0.5 + rng.gen_range(0.0..1.0)); 279 | let new_value = json!(new_value.abs()); 280 | 281 | println!("setting new humidity level: {}", new_value); 282 | 283 | { 284 | let mut t = t.write().unwrap(); 285 | let prop = t.find_property(&"level".to_owned()).unwrap(); 286 | let _ = prop.set_cached_value(new_value.clone()); 287 | } 288 | 289 | t.write() 290 | .unwrap() 291 | .property_notify("level".to_owned(), new_value); 292 | } 293 | }); 294 | 295 | // If adding more than one thing, use ThingsType::Multiple() with a name. 296 | // In the single thing case, the thing's name will be broadcast. 297 | let mut server = WebThingServer::new( 298 | ThingsType::Multiple(things, "LightAndTempDevice".to_owned()), 299 | Some(8888), 300 | None, 301 | None, 302 | Box::new(Generator), 303 | None, 304 | None, 305 | ); 306 | server.start(None).await 307 | } 308 | -------------------------------------------------------------------------------- /examples/single-thing.rs: -------------------------------------------------------------------------------- 1 | use serde_json::json; 2 | use std::sync::{Arc, RwLock, Weak}; 3 | use std::{thread, time}; 4 | use uuid::Uuid; 5 | use webthing::{ 6 | Action, BaseAction, BaseEvent, BaseProperty, BaseThing, Thing, ThingsType, WebThingServer, 7 | }; 8 | 9 | use webthing::server::ActionGenerator; 10 | 11 | pub struct FadeAction(BaseAction); 12 | 13 | impl FadeAction { 14 | fn new( 15 | input: Option>, 16 | thing: Weak>>, 17 | ) -> FadeAction { 18 | FadeAction(BaseAction::new( 19 | Uuid::new_v4().to_string(), 20 | "fade".to_owned(), 21 | input, 22 | thing, 23 | )) 24 | } 25 | } 26 | 27 | impl Action for FadeAction { 28 | fn set_href_prefix(&mut self, prefix: String) { 29 | self.0.set_href_prefix(prefix) 30 | } 31 | 32 | fn get_id(&self) -> String { 33 | self.0.get_id() 34 | } 35 | 36 | fn get_name(&self) -> String { 37 | self.0.get_name() 38 | } 39 | 40 | fn get_href(&self) -> String { 41 | self.0.get_href() 42 | } 43 | 44 | fn get_status(&self) -> String { 45 | self.0.get_status() 46 | } 47 | 48 | fn get_time_requested(&self) -> String { 49 | self.0.get_time_requested() 50 | } 51 | 52 | fn get_time_completed(&self) -> Option { 53 | self.0.get_time_completed() 54 | } 55 | 56 | fn get_input(&self) -> Option> { 57 | self.0.get_input() 58 | } 59 | 60 | fn get_thing(&self) -> Option>>> { 61 | self.0.get_thing() 62 | } 63 | 64 | fn set_status(&mut self, status: String) { 65 | self.0.set_status(status) 66 | } 67 | 68 | fn start(&mut self) { 69 | self.0.start() 70 | } 71 | 72 | fn perform_action(&mut self) { 73 | let thing = self.get_thing(); 74 | if thing.is_none() { 75 | return; 76 | } 77 | 78 | let thing = thing.unwrap(); 79 | let input = self.get_input().unwrap(); 80 | let name = self.get_name(); 81 | let id = self.get_id(); 82 | 83 | thread::spawn(move || { 84 | thread::sleep(time::Duration::from_millis( 85 | input.get("duration").unwrap().as_u64().unwrap(), 86 | )); 87 | 88 | let thing = thing.clone(); 89 | let mut thing = thing.write().unwrap(); 90 | let _ = thing.set_property( 91 | "brightness".to_owned(), 92 | input.get("brightness").unwrap().clone(), 93 | ); 94 | thing.add_event(Box::new(BaseEvent::new( 95 | "overheated".to_owned(), 96 | Some(json!(102)), 97 | ))); 98 | 99 | thing.finish_action(name, id); 100 | }); 101 | } 102 | 103 | fn cancel(&mut self) { 104 | self.0.cancel() 105 | } 106 | 107 | fn finish(&mut self) { 108 | self.0.finish() 109 | } 110 | } 111 | 112 | struct Generator; 113 | 114 | impl ActionGenerator for Generator { 115 | fn generate( 116 | &self, 117 | thing: Weak>>, 118 | name: String, 119 | input: Option<&serde_json::Value>, 120 | ) -> Option> { 121 | let input = match input { 122 | Some(v) => v.as_object().cloned(), 123 | None => None, 124 | }; 125 | 126 | let name: &str = &name; 127 | match name { 128 | "fade" => Some(Box::new(FadeAction::new(input, thing))), 129 | _ => None, 130 | } 131 | } 132 | } 133 | 134 | fn make_thing() -> Arc>> { 135 | let mut thing = BaseThing::new( 136 | "urn:dev:ops:my-lamp-1234".to_owned(), 137 | "My Lamp".to_owned(), 138 | Some(vec!["OnOffSwitch".to_owned(), "Light".to_owned()]), 139 | Some("A web connected lamp".to_owned()), 140 | ); 141 | 142 | let on_description = json!({ 143 | "@type": "OnOffProperty", 144 | "title": "On/Off", 145 | "type": "boolean", 146 | "description": "Whether the lamp is turned on" 147 | }); 148 | let on_description = on_description.as_object().unwrap().clone(); 149 | thing.add_property(Box::new(BaseProperty::new( 150 | "on".to_owned(), 151 | json!(true), 152 | None, 153 | Some(on_description), 154 | ))); 155 | 156 | let brightness_description = json!({ 157 | "@type": "BrightnessProperty", 158 | "title": "Brightness", 159 | "type": "integer", 160 | "description": "The level of light from 0-100", 161 | "minimum": 0, 162 | "maximum": 100, 163 | "unit": "percent" 164 | }); 165 | let brightness_description = brightness_description.as_object().unwrap().clone(); 166 | thing.add_property(Box::new(BaseProperty::new( 167 | "brightness".to_owned(), 168 | json!(50), 169 | None, 170 | Some(brightness_description), 171 | ))); 172 | 173 | let fade_metadata = json!({ 174 | "title": "Fade", 175 | "description": "Fade the lamp to a given level", 176 | "input": { 177 | "type": "object", 178 | "required": [ 179 | "brightness", 180 | "duration" 181 | ], 182 | "properties": { 183 | "brightness": { 184 | "type": "integer", 185 | "minimum": 0, 186 | "maximum": 100, 187 | "unit": "percent" 188 | }, 189 | "duration": { 190 | "type": "integer", 191 | "minimum": 1, 192 | "unit": "milliseconds" 193 | } 194 | } 195 | } 196 | }); 197 | let fade_metadata = fade_metadata.as_object().unwrap().clone(); 198 | thing.add_available_action("fade".to_owned(), fade_metadata); 199 | 200 | let overheated_metadata = json!({ 201 | "description": "The lamp has exceeded its safe operating temperature", 202 | "type": "number", 203 | "unit": "degree celsius" 204 | }); 205 | let overheated_metadata = overheated_metadata.as_object().unwrap().clone(); 206 | thing.add_available_event("overheated".to_owned(), overheated_metadata); 207 | 208 | Arc::new(RwLock::new(Box::new(thing))) 209 | } 210 | 211 | #[actix_rt::main] 212 | async fn main() -> std::io::Result<()> { 213 | env_logger::init(); 214 | let thing = make_thing(); 215 | 216 | // If adding more than one thing, use ThingsType::Multiple() with a name. 217 | // In the single thing case, the thing's name will be broadcast. 218 | let mut server = WebThingServer::new( 219 | ThingsType::Single(thing), 220 | Some(8888), 221 | None, 222 | None, 223 | Box::new(Generator), 224 | None, 225 | None, 226 | ); 227 | server.start(None).await 228 | } 229 | -------------------------------------------------------------------------------- /src/action.rs: -------------------------------------------------------------------------------- 1 | use serde_json; 2 | use serde_json::json; 3 | use std::marker::{Send, Sync}; 4 | use std::sync::{Arc, RwLock, Weak}; 5 | 6 | use super::thing::Thing; 7 | use super::utils::timestamp; 8 | 9 | /// High-level Action trait. 10 | pub trait Action: Send + Sync { 11 | /// Get the action description. 12 | /// 13 | /// Returns a JSON map describing the action. 14 | fn as_action_description(&self) -> serde_json::Map { 15 | let mut description = serde_json::Map::new(); 16 | let mut inner = serde_json::Map::new(); 17 | inner.insert("href".to_owned(), json!(self.get_href())); 18 | inner.insert("timeRequested".to_owned(), json!(self.get_time_requested())); 19 | inner.insert("status".to_owned(), json!(self.get_status())); 20 | 21 | if let Some(input) = self.get_input() { 22 | inner.insert("input".to_owned(), json!(input)); 23 | } 24 | 25 | if let Some(time_completed) = self.get_time_completed() { 26 | inner.insert("timeCompleted".to_owned(), json!(time_completed)); 27 | } 28 | 29 | description.insert(self.get_name(), json!(inner)); 30 | description 31 | } 32 | 33 | /// Set the prefix of any hrefs associated with this action. 34 | fn set_href_prefix(&mut self, prefix: String); 35 | 36 | /// Get this action's ID. 37 | fn get_id(&self) -> String; 38 | 39 | /// Get this action's name. 40 | fn get_name(&self) -> String; 41 | 42 | /// Get this action's href. 43 | fn get_href(&self) -> String; 44 | 45 | /// Get this action's status. 46 | fn get_status(&self) -> String; 47 | 48 | /// Get the thing associated with this action. 49 | fn get_thing(&self) -> Option>>>; 50 | 51 | /// Get the time the action was requested. 52 | fn get_time_requested(&self) -> String; 53 | 54 | /// Get the time the action was completed. 55 | fn get_time_completed(&self) -> Option; 56 | 57 | /// Get the inputs for this action. 58 | fn get_input(&self) -> Option>; 59 | 60 | /// Set the status of this action. 61 | fn set_status(&mut self, status: String); 62 | 63 | /// Start performing the action. 64 | fn start(&mut self); 65 | 66 | /// Override this with the code necessary to perform the action. 67 | fn perform_action(&mut self); 68 | 69 | /// Override this with the code necessary to cancel the action. 70 | fn cancel(&mut self); 71 | 72 | /// Finish performing the action. 73 | fn finish(&mut self); 74 | } 75 | 76 | /// Basic action implementation. 77 | /// 78 | /// An Action represents an individual action which can be performed on a thing. 79 | /// 80 | /// This can easily be used by other actions to handle most of the boring work. 81 | pub struct BaseAction { 82 | id: String, 83 | name: String, 84 | input: Option>, 85 | href_prefix: String, 86 | href: String, 87 | status: String, 88 | time_requested: String, 89 | time_completed: Option, 90 | thing: Weak>>, 91 | } 92 | 93 | impl BaseAction { 94 | /// Create a new BaseAction. 95 | pub fn new( 96 | id: String, 97 | name: String, 98 | input: Option>, 99 | thing: Weak>>, 100 | ) -> Self { 101 | let href = format!("/actions/{}/{}", name, id); 102 | 103 | Self { 104 | id, 105 | name, 106 | input, 107 | href_prefix: "".to_owned(), 108 | href, 109 | status: "created".to_owned(), 110 | time_requested: timestamp(), 111 | time_completed: None, 112 | thing, 113 | } 114 | } 115 | } 116 | 117 | /// An Action represents an individual action on a thing. 118 | impl Action for BaseAction { 119 | /// Set the prefix of any hrefs associated with this action. 120 | fn set_href_prefix(&mut self, prefix: String) { 121 | self.href_prefix = prefix; 122 | } 123 | 124 | /// Get this action's ID. 125 | fn get_id(&self) -> String { 126 | self.id.clone() 127 | } 128 | 129 | /// Get this action's name. 130 | fn get_name(&self) -> String { 131 | self.name.clone() 132 | } 133 | 134 | /// Get this action's href. 135 | fn get_href(&self) -> String { 136 | format!("{}{}", self.href_prefix, self.href) 137 | } 138 | 139 | /// Get this action's status. 140 | fn get_status(&self) -> String { 141 | self.status.clone() 142 | } 143 | 144 | /// Get the thing associated with this action. 145 | fn get_thing(&self) -> Option>>> { 146 | self.thing.upgrade() 147 | } 148 | 149 | /// Get the time the action was requested. 150 | fn get_time_requested(&self) -> String { 151 | self.time_requested.clone() 152 | } 153 | 154 | /// Get the time the action was completed. 155 | fn get_time_completed(&self) -> Option { 156 | self.time_completed.clone() 157 | } 158 | 159 | /// Get the inputs for this action. 160 | fn get_input(&self) -> Option> { 161 | self.input.clone() 162 | } 163 | 164 | /// Set the status of this action. 165 | fn set_status(&mut self, status: String) { 166 | self.status = status; 167 | } 168 | 169 | /// Start performing the action. 170 | fn start(&mut self) { 171 | self.set_status("pending".to_owned()); 172 | } 173 | 174 | /// Override this with the code necessary to perform the action. 175 | fn perform_action(&mut self) {} 176 | 177 | /// Override this with the code necessary to cancel the action. 178 | fn cancel(&mut self) {} 179 | 180 | /// Finish performing the action. 181 | fn finish(&mut self) { 182 | self.set_status("completed".to_owned()); 183 | self.time_completed = Some(timestamp()); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/action_generator.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{RwLock, Weak}; 2 | 3 | use super::action::Action; 4 | use super::thing::Thing; 5 | 6 | /// Generator for new actions, based on name. 7 | pub trait ActionGenerator: Send + Sync { 8 | /// Generate a new action, if possible. 9 | /// 10 | /// # Arguments 11 | /// 12 | /// * `thing` - thing associated with this action 13 | /// * `name` - name of the requested action 14 | /// * `input` - input for the action 15 | fn generate( 16 | &self, 17 | thing: Weak>>, 18 | name: String, 19 | input: Option<&serde_json::Value>, 20 | ) -> Option>; 21 | } 22 | 23 | /// Basic action generator implementation. 24 | /// 25 | /// This always returns `None` and can be used when no actions are needed. 26 | pub struct BaseActionGenerator; 27 | 28 | impl ActionGenerator for BaseActionGenerator { 29 | fn generate( 30 | &self, 31 | _thing: Weak>>, 32 | _name: String, 33 | _input: Option<&serde_json::Value>, 34 | ) -> Option> { 35 | None 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use serde_json; 2 | use serde_json::json; 3 | use std::marker::{Send, Sync}; 4 | 5 | use super::utils::timestamp; 6 | 7 | /// High-level Event trait. 8 | pub trait Event: Send + Sync { 9 | /// Get the event description. 10 | /// 11 | /// Returns a JSON map describing the event. 12 | fn as_event_description(&self) -> serde_json::Map { 13 | let mut description = serde_json::Map::new(); 14 | let mut inner = serde_json::Map::new(); 15 | inner.insert("timestamp".to_string(), json!(self.get_time())); 16 | 17 | let data = self.get_data(); 18 | if data.is_some() { 19 | inner.insert("data".to_string(), json!(data)); 20 | } 21 | 22 | description.insert(self.get_name(), json!(inner)); 23 | description 24 | } 25 | 26 | /// Get the event's name. 27 | fn get_name(&self) -> String; 28 | 29 | /// Get the event's data. 30 | fn get_data(&self) -> Option; 31 | 32 | /// Get the event's timestamp. 33 | fn get_time(&self) -> String; 34 | } 35 | 36 | /// Basic event implementation. 37 | /// 38 | /// An Event represents an individual event from a thing. 39 | /// 40 | /// This can easily be used by other events to handle most of the boring work. 41 | pub struct BaseEvent { 42 | name: String, 43 | data: Option, 44 | time: String, 45 | } 46 | 47 | impl BaseEvent { 48 | /// Create a new BaseEvent. 49 | pub fn new(name: String, data: Option) -> Self { 50 | Self { 51 | name, 52 | data, 53 | time: timestamp(), 54 | } 55 | } 56 | } 57 | 58 | impl Event for BaseEvent { 59 | /// Get the event's name. 60 | fn get_name(&self) -> String { 61 | self.name.clone() 62 | } 63 | 64 | /// Get the event's data. 65 | fn get_data(&self) -> Option { 66 | self.data.clone() 67 | } 68 | 69 | /// Get the event's timestamp. 70 | fn get_time(&self) -> String { 71 | self.time.clone() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | //! Implementation of an HTTP [Web Thing](https://webthings.io/api/). 4 | 5 | extern crate std; 6 | 7 | /// Action trait and base implementation. 8 | pub mod action; 9 | 10 | /// ActionGenerator trait and base implementation. 11 | pub mod action_generator; 12 | 13 | /// Event trait and base implementation. 14 | pub mod event; 15 | 16 | /// Property trait and base implementation. 17 | pub mod property; 18 | 19 | /// WebThingServer implementation. 20 | #[cfg(feature = "actix")] 21 | pub mod server; 22 | 23 | /// Thing trait and base implementation. 24 | pub mod thing; 25 | 26 | /// Utility functions. 27 | pub mod utils; 28 | 29 | pub use action::{Action, BaseAction}; 30 | pub use action_generator::BaseActionGenerator; 31 | pub use event::{BaseEvent, Event}; 32 | pub use property::{BaseProperty, Property}; 33 | 34 | #[cfg(feature = "actix")] 35 | pub use server::{ThingsType, WebThingServer}; 36 | 37 | pub use thing::{BaseThing, Thing, ThingContext}; 38 | -------------------------------------------------------------------------------- /src/property.rs: -------------------------------------------------------------------------------- 1 | use serde_json; 2 | use serde_json::json; 3 | use std::marker::{Send, Sync}; 4 | use valico::json_schema; 5 | 6 | /// Used to forward a new property value to the physical/virtual device. 7 | pub trait ValueForwarder: Send + Sync { 8 | /// Set the new value of the property. 9 | fn set_value(&mut self, value: serde_json::Value) -> Result; 10 | } 11 | 12 | /// High-level Property trait. 13 | pub trait Property: Send + Sync { 14 | /// Validate new property value before setting it. 15 | /// 16 | /// Returns a result indicating validity. 17 | fn validate_value(&self, value: &serde_json::Value) -> Result<(), &'static str> { 18 | let mut description = self.get_metadata(); 19 | description.remove("@type"); 20 | description.remove("unit"); 21 | description.remove("title"); 22 | 23 | if description 24 | .get("readOnly") 25 | .and_then(|b| b.as_bool()) 26 | .unwrap_or(false) 27 | { 28 | return Err("Read-only property"); 29 | } 30 | 31 | let mut scope = json_schema::Scope::new(); 32 | match scope.compile_and_return(json!(description), true) { 33 | Ok(validator) => { 34 | if validator.validate(value).is_valid() { 35 | Ok(()) 36 | } else { 37 | Err("Invalid property value") 38 | } 39 | } 40 | Err(_) => Err("Invalid property schema"), 41 | } 42 | } 43 | 44 | /// Get the property description. 45 | /// 46 | /// Returns a JSON value describing the property. 47 | fn as_property_description(&self) -> serde_json::Map { 48 | let mut description = self.get_metadata(); 49 | let link = json!( 50 | { 51 | "rel": "property", 52 | "href": self.get_href(), 53 | } 54 | ); 55 | 56 | if let Some(links) = description 57 | .get_mut("links") 58 | .map(|links| links.as_array_mut().unwrap()) 59 | { 60 | links.push(link); 61 | } else { 62 | description.insert("links".to_string(), json!([link])); 63 | } 64 | description 65 | } 66 | 67 | /// Set the prefix of any hrefs associated with this property. 68 | fn set_href_prefix(&mut self, prefix: String); 69 | 70 | /// Get the href of this property. 71 | fn get_href(&self) -> String; 72 | 73 | /// Get the current property value. 74 | fn get_value(&self) -> serde_json::Value; 75 | 76 | /// Set the current value of the property with the value forwarder. 77 | fn set_value(&mut self, value: serde_json::Value) -> Result<(), &'static str>; 78 | 79 | /// Set the cached value of the property. 80 | fn set_cached_value(&mut self, value: serde_json::Value) -> Result<(), &'static str>; 81 | 82 | /// Get the name of this property. 83 | fn get_name(&self) -> String; 84 | 85 | /// Get the metadata associated with this property. 86 | fn get_metadata(&self) -> serde_json::Map; 87 | } 88 | 89 | /// Basic property implementation. 90 | /// 91 | /// A Property represents an individual state value of a thing. 92 | /// 93 | /// This can easily be used by other properties to handle most of the boring work. 94 | pub struct BaseProperty { 95 | name: String, 96 | last_value: serde_json::Value, 97 | value_forwarder: Option>, 98 | href_prefix: String, 99 | href: String, 100 | metadata: serde_json::Map, 101 | } 102 | 103 | impl BaseProperty { 104 | /// Create a new BaseProperty. 105 | /// 106 | /// # Arguments 107 | /// 108 | /// * `name` - name of the property 109 | /// * `initial_value` - initial property value 110 | /// * `value_forwarder` - optional value forwarder; property will be read-only if None 111 | /// * `metadata` - property metadata, i.e. type, description, unit, etc., as a JSON map 112 | pub fn new( 113 | name: String, 114 | initial_value: serde_json::Value, 115 | value_forwarder: Option>, 116 | metadata: Option>, 117 | ) -> BaseProperty { 118 | let meta = match metadata { 119 | Some(m) => m, 120 | None => serde_json::Map::new(), 121 | }; 122 | 123 | let href = format!("/properties/{}", name); 124 | 125 | BaseProperty { 126 | name, 127 | last_value: initial_value, 128 | value_forwarder, 129 | href_prefix: "".to_owned(), 130 | href, 131 | metadata: meta, 132 | } 133 | } 134 | } 135 | 136 | impl Property for BaseProperty { 137 | /// Set the prefix of any hrefs associated with this property. 138 | fn set_href_prefix(&mut self, prefix: String) { 139 | self.href_prefix = prefix; 140 | } 141 | 142 | /// Get the href of this property. 143 | fn get_href(&self) -> String { 144 | format!("{}{}", self.href_prefix, self.href) 145 | } 146 | 147 | /// Get the current property value. 148 | fn get_value(&self) -> serde_json::Value { 149 | self.last_value.clone() 150 | } 151 | 152 | /// Set the current value of the property. 153 | fn set_value(&mut self, value: serde_json::Value) -> Result<(), &'static str> { 154 | self.validate_value(&value)?; 155 | 156 | match self.value_forwarder { 157 | Some(ref mut vf) => match vf.set_value(value) { 158 | Ok(v) => { 159 | self.last_value = v; 160 | Ok(()) 161 | } 162 | Err(e) => Err(e), 163 | }, 164 | None => { 165 | self.last_value = value; 166 | Ok(()) 167 | } 168 | } 169 | } 170 | 171 | /// Set the cached value of the property. 172 | fn set_cached_value(&mut self, value: serde_json::Value) -> Result<(), &'static str> { 173 | self.last_value = value; 174 | Ok(()) 175 | } 176 | 177 | /// Get the name of this property. 178 | fn get_name(&self) -> String { 179 | self.name.clone() 180 | } 181 | 182 | /// Get the metadata associated with this property. 183 | fn get_metadata(&self) -> serde_json::Map { 184 | self.metadata.clone() 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | /// Rust Web Thing server implementation. 2 | use actix; 3 | use actix::prelude::*; 4 | use actix_web; 5 | use actix_web::body::EitherBody; 6 | use actix_web::dev::{Server, ServiceRequest, ServiceResponse}; 7 | use actix_web::dev::{Service, Transform}; 8 | use actix_web::guard; 9 | use actix_web::http::header::HeaderValue; 10 | use actix_web::web::Data; 11 | use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; 12 | use actix_web_actors::ws; 13 | use futures::future::{ok, LocalBoxFuture, Ready}; 14 | use hostname; 15 | use libmdns; 16 | #[cfg(feature = "ssl")] 17 | use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; 18 | use serde_json; 19 | use serde_json::json; 20 | use std::marker::{Send, Sync}; 21 | use std::sync::{Arc, RwLock}; 22 | use std::task::{Context, Poll}; 23 | use std::time::Duration; 24 | use uuid::Uuid; 25 | 26 | pub use super::action_generator::ActionGenerator; 27 | use super::thing::Thing; 28 | use super::utils::get_addresses; 29 | 30 | const SERVICE_TYPE: &str = "_webthing._tcp"; 31 | 32 | /// Represents the things managed by the server. 33 | #[derive(Clone)] 34 | pub enum ThingsType { 35 | /// Set when there are multiple things managed by the server 36 | Multiple(Vec>>>, String), 37 | /// Set when there is only one thing 38 | Single(Arc>>), 39 | } 40 | 41 | /// Shared app state, used by server threads. 42 | struct AppState { 43 | things: Arc, 44 | hosts: Arc>, 45 | disable_host_validation: Arc, 46 | action_generator: Arc, 47 | } 48 | 49 | impl AppState { 50 | /// Get the thing this request is for. 51 | fn get_thing(&self, thing_id: Option<&str>) -> Option>>> { 52 | match self.things.as_ref() { 53 | ThingsType::Multiple(ref inner_things, _) => { 54 | let id = thing_id?.parse::().ok()?; 55 | if id >= inner_things.len() { 56 | None 57 | } else { 58 | Some(inner_things[id].clone()) 59 | } 60 | } 61 | ThingsType::Single(ref thing) => Some(thing.clone()), 62 | } 63 | } 64 | 65 | fn get_things(&self) -> Arc { 66 | self.things.clone() 67 | } 68 | 69 | fn get_action_generator(&self) -> Arc { 70 | self.action_generator.clone() 71 | } 72 | 73 | fn validate_host(&self, host: Option<&HeaderValue>) -> Result<(), ()> { 74 | if *self.disable_host_validation { 75 | return Ok(()); 76 | } 77 | 78 | if let Some(Ok(host)) = host.map(|h| h.to_str()) { 79 | if self.hosts.contains(&host.to_lowercase()) { 80 | return Ok(()); 81 | } 82 | } 83 | 84 | Err(()) 85 | } 86 | } 87 | 88 | /// Host validation middleware 89 | struct HostValidator; 90 | 91 | impl Transform for HostValidator 92 | where 93 | S: Service, Error = Error>, 94 | S::Future: 'static, 95 | B: 'static, 96 | { 97 | type Response = ServiceResponse>; 98 | type Error = Error; 99 | type InitError = (); 100 | type Transform = HostValidatorMiddleware; 101 | type Future = Ready>; 102 | 103 | fn new_transform(&self, service: S) -> Self::Future { 104 | ok(HostValidatorMiddleware { service }) 105 | } 106 | } 107 | 108 | struct HostValidatorMiddleware> { 109 | service: S, 110 | } 111 | 112 | impl Service for HostValidatorMiddleware 113 | where 114 | S: Service, Error = Error>, 115 | S::Future: 'static, 116 | B: 'static, 117 | { 118 | type Response = ServiceResponse>; 119 | type Error = Error; 120 | type Future = LocalBoxFuture<'static, Result>; 121 | 122 | fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { 123 | self.service.poll_ready(cx) 124 | } 125 | 126 | fn call(&self, req: ServiceRequest) -> Self::Future { 127 | if let Some(state) = req.app_data::>() { 128 | let host = req.headers().get("Host"); 129 | match state.validate_host(host) { 130 | Ok(_) => { 131 | let res = self.service.call(req); 132 | Box::pin(async move { res.await.map(ServiceResponse::map_into_left_body) }) 133 | } 134 | Err(_) => Box::pin(async { 135 | Ok(req.into_response(HttpResponse::Forbidden().finish().map_into_right_body())) 136 | }), 137 | } 138 | } else { 139 | Box::pin(async { 140 | Ok(req.into_response(HttpResponse::Forbidden().finish().map_into_right_body())) 141 | }) 142 | } 143 | } 144 | } 145 | 146 | /// Shared state used by individual websockets. 147 | struct ThingWebSocket { 148 | id: String, 149 | thing_id: usize, 150 | things: Arc, 151 | action_generator: Arc, 152 | } 153 | 154 | impl ThingWebSocket { 155 | /// Get the ID of this websocket. 156 | fn get_id(&self) -> String { 157 | self.id.clone() 158 | } 159 | 160 | /// Get the thing associated with this websocket. 161 | fn get_thing(&self) -> Arc>> { 162 | match self.things.as_ref() { 163 | ThingsType::Multiple(ref things, _) => things[self.thing_id].clone(), 164 | ThingsType::Single(ref thing) => thing.clone(), 165 | } 166 | } 167 | 168 | /// Drain all message queues associated with this websocket. 169 | fn drain_queue(&self, ctx: &mut ws::WebsocketContext) { 170 | ctx.run_later(Duration::from_millis(200), |act, ctx| { 171 | let thing = act.get_thing(); 172 | let mut thing = thing.write().unwrap(); 173 | 174 | let drains = thing.drain_queue(act.get_id()); 175 | for iter in drains { 176 | for message in iter { 177 | ctx.text(message); 178 | } 179 | } 180 | 181 | act.drain_queue(ctx); 182 | }); 183 | } 184 | } 185 | 186 | impl Actor for ThingWebSocket { 187 | type Context = ws::WebsocketContext; 188 | } 189 | 190 | fn bad_request(message: impl AsRef, request: Option) -> serde_json::Value { 191 | if let Some(request) = request { 192 | json!({ 193 | "messageType": "error", 194 | "data": { 195 | "status": "400 Bad Request", 196 | "message": message.as_ref(), 197 | "request": request, 198 | } 199 | }) 200 | } else { 201 | json!({ 202 | "messageType": "error", 203 | "data": { 204 | "status": "400 Bad Request", 205 | "message": message.as_ref(), 206 | } 207 | }) 208 | } 209 | } 210 | 211 | fn bad_request_string(message: impl AsRef, request: Option) -> String { 212 | serde_json::to_string(&bad_request(message, request)).unwrap() 213 | } 214 | 215 | impl StreamHandler> for ThingWebSocket { 216 | fn started(&mut self, ctx: &mut Self::Context) { 217 | self.drain_queue(ctx); 218 | } 219 | 220 | fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { 221 | match msg { 222 | Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), 223 | Ok(ws::Message::Pong(_)) => (), 224 | Ok(ws::Message::Text(text)) => { 225 | let message: serde_json::Value = if let Ok(message) = serde_json::from_str(&text) { 226 | message 227 | } else { 228 | return ctx.text(bad_request_string("Parsing request failed", None)); 229 | }; 230 | 231 | let message = if let Some(object) = message.as_object() { 232 | object 233 | } else { 234 | return ctx.text(bad_request_string("Parsing request failed", Some(message))); 235 | }; 236 | 237 | if !message.contains_key("messageType") || !message.contains_key("data") { 238 | return ctx.text(bad_request_string("Invalid message", Some(json!(message)))); 239 | } 240 | 241 | let msg_type = message.get("messageType").unwrap().as_str(); 242 | let data = message.get("data").unwrap().as_object(); 243 | if msg_type.is_none() || data.is_none() { 244 | return ctx.text(bad_request_string("Invalid message", Some(json!(message)))); 245 | } 246 | 247 | let msg_type = msg_type.unwrap(); 248 | let data = data.unwrap(); 249 | let thing = self.get_thing(); 250 | 251 | match msg_type { 252 | "setProperty" => { 253 | for (property_name, property_value) in data.iter() { 254 | let result = thing 255 | .write() 256 | .unwrap() 257 | .set_property(property_name.to_string(), property_value.clone()); 258 | 259 | if let Err(err) = result { 260 | return ctx.text(bad_request_string(err, Some(json!(message)))); 261 | } 262 | } 263 | } 264 | "requestAction" => { 265 | for (action_name, action_params) in data.iter() { 266 | let input = action_params.get("input"); 267 | let action = self.action_generator.generate( 268 | Arc::downgrade(&self.get_thing()), 269 | action_name.to_string(), 270 | input, 271 | ); 272 | 273 | if action.is_none() { 274 | return ctx.text(bad_request_string( 275 | "Invalid action request", 276 | Some(json!(message)), 277 | )); 278 | } 279 | 280 | let action = action.unwrap(); 281 | let id = action.get_id(); 282 | let action = Arc::new(RwLock::new(action)); 283 | 284 | { 285 | let mut thing = thing.write().unwrap(); 286 | if let Err(err) = thing.add_action(action.clone(), input) { 287 | return ctx.text(bad_request_string( 288 | format!("Failed to start action: {}", err), 289 | Some(json!(message)), 290 | )); 291 | } 292 | } 293 | 294 | thing 295 | .write() 296 | .unwrap() 297 | .start_action(action_name.to_string(), id); 298 | } 299 | } 300 | "addEventSubscription" => { 301 | for event_name in data.keys() { 302 | thing 303 | .write() 304 | .unwrap() 305 | .add_event_subscriber(event_name.to_string(), self.get_id()); 306 | } 307 | } 308 | unknown => { 309 | return ctx.text(bad_request_string( 310 | format!("Unknown messageType: {}", unknown), 311 | Some(json!(message)), 312 | )); 313 | } 314 | } 315 | } 316 | Ok(ws::Message::Close(_)) => { 317 | let thing = self.get_thing(); 318 | thing.write().unwrap().remove_subscriber(self.get_id()); 319 | } 320 | _ => (), 321 | } 322 | } 323 | } 324 | 325 | /// Handle a GET request to / when the server manages multiple things. 326 | async fn handle_get_things(req: HttpRequest, state: web::Data) -> HttpResponse { 327 | let mut response: Vec> = Vec::new(); 328 | 329 | // The host header is already checked by HostValidator, so the unwrapping is safe here. 330 | let host = req.headers().get("Host").unwrap().to_str().unwrap(); 331 | let connection = req.connection_info(); 332 | let scheme = connection.scheme(); 333 | let ws_href = format!( 334 | "{}://{}", 335 | if scheme == "https" { "wss" } else { "ws" }, 336 | host 337 | ); 338 | 339 | if let ThingsType::Multiple(things, _) = state.things.as_ref() { 340 | for thing in things.iter() { 341 | let thing = thing.read().unwrap(); 342 | 343 | let mut link = serde_json::Map::new(); 344 | link.insert("rel".to_owned(), json!("alternate")); 345 | link.insert( 346 | "href".to_owned(), 347 | json!(format!("{}{}", ws_href, thing.get_href())), 348 | ); 349 | 350 | let mut description = thing.as_thing_description().clone(); 351 | { 352 | let links = description 353 | .get_mut("links") 354 | .unwrap() 355 | .as_array_mut() 356 | .unwrap(); 357 | links.push(json!(link)); 358 | } 359 | 360 | description.insert("href".to_owned(), json!(thing.get_href())); 361 | description.insert( 362 | "base".to_owned(), 363 | json!(format!("{}://{}{}", scheme, host, thing.get_href())), 364 | ); 365 | description.insert( 366 | "securityDefinitions".to_owned(), 367 | json!({"nosec_sc": {"scheme": "nosec"}}), 368 | ); 369 | description.insert("security".to_owned(), json!("nosec_sc")); 370 | 371 | response.push(description); 372 | } 373 | } 374 | HttpResponse::Ok().json(response) 375 | } 376 | 377 | /// Handle a GET request to /. 378 | async fn handle_get_thing(req: HttpRequest, state: web::Data) -> HttpResponse { 379 | let thing = state.get_thing(req.match_info().get("thing_id")); 380 | match thing { 381 | None => HttpResponse::NotFound().finish(), 382 | Some(thing) => { 383 | let thing = thing.read().unwrap(); 384 | 385 | // The host header is already checked by HostValidator, so the unwrapping is safe here. 386 | let host = req.headers().get("Host").unwrap().to_str().unwrap(); 387 | let connection = req.connection_info(); 388 | let scheme = connection.scheme(); 389 | let ws_href = format!( 390 | "{}://{}{}", 391 | if scheme == "https" { "wss" } else { "ws" }, 392 | host, 393 | thing.get_href() 394 | ); 395 | 396 | let mut link = serde_json::Map::new(); 397 | link.insert("rel".to_owned(), json!("alternate")); 398 | link.insert("href".to_owned(), json!(ws_href)); 399 | 400 | let mut description = thing.as_thing_description(); 401 | { 402 | let links = description 403 | .get_mut("links") 404 | .unwrap() 405 | .as_array_mut() 406 | .unwrap(); 407 | links.push(json!(link)); 408 | } 409 | 410 | description.insert( 411 | "base".to_owned(), 412 | json!(format!("{}://{}{}", scheme, host, thing.get_href())), 413 | ); 414 | description.insert( 415 | "securityDefinitions".to_owned(), 416 | json!({"nosec_sc": {"scheme": "nosec"}}), 417 | ); 418 | description.insert("security".to_owned(), json!("nosec_sc")); 419 | 420 | HttpResponse::Ok().json(description) 421 | } 422 | } 423 | } 424 | 425 | /// Handle websocket on /. 426 | async fn handle_ws_thing( 427 | req: HttpRequest, 428 | state: web::Data, 429 | stream: web::Payload, 430 | ) -> Result { 431 | let thing_id = req.match_info().get("thing_id"); 432 | 433 | match state.get_thing(thing_id) { 434 | None => Ok(HttpResponse::NotFound().finish()), 435 | Some(thing) => { 436 | let thing_id = match thing_id { 437 | None => 0, 438 | Some(id) => id.parse::().unwrap(), 439 | }; 440 | let ws = ThingWebSocket { 441 | id: Uuid::new_v4().to_string(), 442 | thing_id, 443 | things: state.get_things(), 444 | action_generator: state.get_action_generator(), 445 | }; 446 | thing.write().unwrap().add_subscriber(ws.get_id()); 447 | ws::start(ws, &req, stream) 448 | } 449 | } 450 | } 451 | 452 | /// Handle a GET request to /properties. 453 | async fn handle_get_properties(req: HttpRequest, state: web::Data) -> HttpResponse { 454 | if let Some(thing) = state.get_thing(req.match_info().get("thing_id")) { 455 | let thing = thing.read().unwrap(); 456 | HttpResponse::Ok().json(json!(thing.get_properties())) 457 | } else { 458 | HttpResponse::NotFound().finish() 459 | } 460 | } 461 | 462 | /// Handle a GET request to /properties/. 463 | async fn handle_get_property(req: HttpRequest, state: web::Data) -> HttpResponse { 464 | let thing = match state.get_thing(req.match_info().get("thing_id")) { 465 | Some(thing) => thing, 466 | None => return HttpResponse::NotFound().finish(), 467 | }; 468 | 469 | let property_name = match req.match_info().get("property_name") { 470 | Some(property_name) => property_name, 471 | None => return HttpResponse::NotFound().finish(), 472 | }; 473 | 474 | let thing = thing.read().unwrap(); 475 | if thing.has_property(&property_name.to_string()) { 476 | HttpResponse::Ok() 477 | .json(json!({property_name: thing.get_property(&property_name.to_string()).unwrap()})) 478 | } else { 479 | HttpResponse::NotFound().finish() 480 | } 481 | } 482 | 483 | /// Handle a PUT request to /properties/. 484 | async fn handle_put_property( 485 | req: HttpRequest, 486 | state: web::Data, 487 | body: web::Json, 488 | ) -> HttpResponse { 489 | let thing = match state.get_thing(req.match_info().get("thing_id")) { 490 | Some(thing) => thing, 491 | None => return HttpResponse::NotFound().finish(), 492 | }; 493 | 494 | let property_name = match req.match_info().get("property_name") { 495 | Some(property_name) => property_name, 496 | None => return HttpResponse::NotFound().finish(), 497 | }; 498 | 499 | let args = match body.as_object() { 500 | Some(args) => args, 501 | None => { 502 | return HttpResponse::BadRequest().json(bad_request( 503 | "Parsing request failed", 504 | Some(body.into_inner()), 505 | )) 506 | } 507 | }; 508 | 509 | let arg = if let Some(arg) = args.get(property_name) { 510 | arg 511 | } else { 512 | return HttpResponse::BadRequest().json(bad_request( 513 | "Request does not contain property key", 514 | Some(json!(args)), 515 | )); 516 | }; 517 | 518 | let mut thing = thing.write().unwrap(); 519 | if thing.has_property(&property_name.to_string()) { 520 | let set_property_result = thing.set_property(property_name.to_string(), arg.clone()); 521 | 522 | match set_property_result { 523 | Ok(()) => HttpResponse::Ok().json( 524 | json!({property_name: thing.get_property(&property_name.to_string()).unwrap()}), 525 | ), 526 | Err(err) => HttpResponse::BadRequest().json(bad_request(err, Some(json!(args)))), 527 | } 528 | } else { 529 | HttpResponse::NotFound().finish() 530 | } 531 | } 532 | 533 | /// Handle a GET request to /actions. 534 | async fn handle_get_actions(req: HttpRequest, state: web::Data) -> HttpResponse { 535 | match state.get_thing(req.match_info().get("thing_id")) { 536 | None => HttpResponse::NotFound().finish(), 537 | Some(thing) => HttpResponse::Ok().json(thing.read().unwrap().get_action_descriptions(None)), 538 | } 539 | } 540 | 541 | /// Handle a POST request to /actions. 542 | async fn handle_post_actions( 543 | req: HttpRequest, 544 | state: web::Data, 545 | body: web::Json, 546 | ) -> HttpResponse { 547 | let thing = match state.get_thing(req.match_info().get("thing_id")) { 548 | Some(thing) => thing, 549 | None => return HttpResponse::NotFound().finish(), 550 | }; 551 | 552 | let message = match body.as_object() { 553 | Some(message) => message, 554 | None => return HttpResponse::BadRequest().finish(), 555 | }; 556 | 557 | let keys: Vec<&String> = message.keys().collect(); 558 | if keys.len() != 1 { 559 | return HttpResponse::BadRequest().finish(); 560 | } 561 | 562 | let action_name = keys[0]; 563 | let action_params = message.get(action_name).unwrap(); 564 | let input = action_params.get("input"); 565 | 566 | let action = state.get_action_generator().generate( 567 | Arc::downgrade(&thing.clone()), 568 | action_name.to_string(), 569 | input, 570 | ); 571 | 572 | if let Some(action) = action { 573 | let id = action.get_id(); 574 | let action = Arc::new(RwLock::new(action)); 575 | 576 | { 577 | let mut thing = thing.write().unwrap(); 578 | let result = thing.add_action(action.clone(), input); 579 | 580 | if result.is_err() { 581 | return HttpResponse::BadRequest().finish(); 582 | } 583 | } 584 | 585 | let mut response: serde_json::Map = serde_json::Map::new(); 586 | response.insert( 587 | action_name.to_string(), 588 | action 589 | .read() 590 | .unwrap() 591 | .as_action_description() 592 | .get(action_name) 593 | .unwrap() 594 | .clone(), 595 | ); 596 | 597 | thing 598 | .write() 599 | .unwrap() 600 | .start_action(action_name.to_string(), id); 601 | 602 | HttpResponse::Created().json(response) 603 | } else { 604 | HttpResponse::BadRequest().finish() 605 | } 606 | } 607 | 608 | /// Handle a GET request to /actions/. 609 | async fn handle_get_action(req: HttpRequest, state: web::Data) -> HttpResponse { 610 | if let Some(thing) = state.get_thing(req.match_info().get("thing_id")) { 611 | if let Some(action_name) = req.match_info().get("action_name") { 612 | let thing = thing.read().unwrap(); 613 | return HttpResponse::Ok() 614 | .json(thing.get_action_descriptions(Some(action_name.to_string()))); 615 | } 616 | } 617 | 618 | HttpResponse::NotFound().finish() 619 | } 620 | 621 | /// Handle a POST request to /actions/. 622 | async fn handle_post_action( 623 | req: HttpRequest, 624 | state: web::Data, 625 | body: web::Json, 626 | ) -> HttpResponse { 627 | let thing = if let Some(thing) = state.get_thing(req.match_info().get("thing_id")) { 628 | thing 629 | } else { 630 | return HttpResponse::NotFound().finish(); 631 | }; 632 | 633 | let action_name = if let Some(action_name) = req.match_info().get("action_name") { 634 | action_name 635 | } else { 636 | return HttpResponse::NotFound().finish(); 637 | }; 638 | 639 | let message = if let Some(message) = body.as_object() { 640 | message 641 | } else { 642 | return HttpResponse::BadRequest().finish(); 643 | }; 644 | 645 | if message.keys().count() != 1 { 646 | return HttpResponse::BadRequest().finish(); 647 | } 648 | 649 | let input = if let Some(action_params) = message.get(action_name) { 650 | action_params.get("input") 651 | } else { 652 | return HttpResponse::BadRequest().finish(); 653 | }; 654 | 655 | let action = state.get_action_generator().generate( 656 | Arc::downgrade(&thing.clone()), 657 | action_name.to_string(), 658 | input, 659 | ); 660 | 661 | if let Some(action) = action { 662 | let id = action.get_id(); 663 | let action = Arc::new(RwLock::new(action)); 664 | 665 | { 666 | let mut thing = thing.write().unwrap(); 667 | let result = thing.add_action(action.clone(), input); 668 | 669 | if result.is_err() { 670 | return HttpResponse::BadRequest().finish(); 671 | } 672 | } 673 | 674 | let mut response: serde_json::Map = serde_json::Map::new(); 675 | response.insert( 676 | action_name.to_string(), 677 | action 678 | .read() 679 | .unwrap() 680 | .as_action_description() 681 | .get(action_name) 682 | .unwrap() 683 | .clone(), 684 | ); 685 | 686 | thing 687 | .write() 688 | .unwrap() 689 | .start_action(action_name.to_string(), id); 690 | 691 | HttpResponse::Created().json(response) 692 | } else { 693 | HttpResponse::BadRequest().finish() 694 | } 695 | } 696 | 697 | /// Handle a GET request to /actions//. 698 | async fn handle_get_action_id(req: HttpRequest, state: web::Data) -> HttpResponse { 699 | let thing = if let Some(thing) = state.get_thing(req.match_info().get("thing_id")) { 700 | thing 701 | } else { 702 | return HttpResponse::NotFound().finish(); 703 | }; 704 | 705 | let action_name = req.match_info().get("action_name"); 706 | let action_id = req.match_info().get("action_id"); 707 | let (action_name, action_id) = if let Some(action) = action_name.zip(action_id) { 708 | action 709 | } else { 710 | return HttpResponse::NotFound().finish(); 711 | }; 712 | 713 | let thing = thing.read().unwrap(); 714 | if let Some(action) = thing.get_action(action_name.to_string(), action_id.to_string()) { 715 | HttpResponse::Ok().json(action.read().unwrap().as_action_description()) 716 | } else { 717 | HttpResponse::NotFound().finish() 718 | } 719 | } 720 | 721 | /// Handle a PUT request to /actions//. 722 | async fn handle_put_action_id( 723 | req: HttpRequest, 724 | state: web::Data, 725 | _body: web::Json, 726 | ) -> HttpResponse { 727 | match state.get_thing(req.match_info().get("thing_id")) { 728 | Some(_) => { 729 | // TODO: this is not yet defined in the spec 730 | HttpResponse::Ok().finish() 731 | } 732 | None => HttpResponse::NotFound().finish(), 733 | } 734 | } 735 | 736 | /// Handle a DELETE request to /actions//. 737 | async fn handle_delete_action_id(req: HttpRequest, state: web::Data) -> HttpResponse { 738 | let thing = match state.get_thing(req.match_info().get("thing_id")) { 739 | Some(thing) => thing, 740 | None => return HttpResponse::NotFound().finish(), 741 | }; 742 | 743 | let action_name = req.match_info().get("action_name"); 744 | let action_id = req.match_info().get("action_id"); 745 | if let Some((action_name, action_id)) = action_name.zip(action_id) { 746 | if thing 747 | .write() 748 | .unwrap() 749 | .remove_action(action_name.to_string(), action_id.to_string()) 750 | { 751 | return HttpResponse::NoContent().finish(); 752 | } 753 | } 754 | 755 | HttpResponse::NotFound().finish() 756 | } 757 | 758 | /// Handle a GET request to /events. 759 | async fn handle_get_events(req: HttpRequest, state: web::Data) -> HttpResponse { 760 | match state.get_thing(req.match_info().get("thing_id")) { 761 | None => HttpResponse::NotFound().finish(), 762 | Some(thing) => HttpResponse::Ok().json(thing.read().unwrap().get_event_descriptions(None)), 763 | } 764 | } 765 | 766 | /// Handle a GET request to /events/. 767 | async fn handle_get_event(req: HttpRequest, state: web::Data) -> HttpResponse { 768 | let thing = match state.get_thing(req.match_info().get("thing_id")) { 769 | Some(thing) => thing, 770 | None => return HttpResponse::NotFound().finish(), 771 | }; 772 | 773 | let event_name = match req.match_info().get("event_name") { 774 | Some(event_name) => event_name, 775 | None => return HttpResponse::NotFound().finish(), 776 | }; 777 | 778 | let thing = thing.read().unwrap(); 779 | HttpResponse::Ok().json(thing.get_event_descriptions(Some(event_name.to_string()))) 780 | } 781 | 782 | /// Server to represent a Web Thing over HTTP. 783 | pub struct WebThingServer { 784 | things: ThingsType, 785 | base_path: String, 786 | disable_host_validation: bool, 787 | port: Option, 788 | hostname: Option, 789 | dns_service: Option, 790 | #[allow(dead_code)] 791 | ssl_options: Option<(String, String)>, 792 | generator_arc: Arc, 793 | } 794 | 795 | impl WebThingServer { 796 | /// Create a new WebThingServer. 797 | /// 798 | /// # Arguments 799 | /// 800 | /// * `things` - list of Things managed by this server 801 | /// * `port` - port to listen on (defaults to 80) 802 | /// * `hostname` - optional host name, i.e. mything.com 803 | /// * `ssl_options` - tuple of SSL options to pass to the actix web server 804 | /// * `action_generator` - action generator struct 805 | /// * `base_path` - base URL to use, rather than '/' 806 | /// * `disable_host_validation` - whether or not to disable host validation -- note that this 807 | /// can lead to DNS rebinding attacks. `None` means to use the default, 808 | /// which keeps it enabled. 809 | pub fn new( 810 | things: ThingsType, 811 | port: Option, 812 | hostname: Option, 813 | ssl_options: Option<(String, String)>, 814 | action_generator: Box, 815 | base_path: Option, 816 | disable_host_validation: Option, 817 | ) -> Self { 818 | Self { 819 | things, 820 | base_path: base_path 821 | .map(|p| p.trim_end_matches('/').to_string()) 822 | .unwrap_or_else(|| "".to_owned()), 823 | disable_host_validation: disable_host_validation.unwrap_or(false), 824 | port, 825 | hostname, 826 | dns_service: None, 827 | ssl_options, 828 | generator_arc: Arc::from(action_generator), 829 | } 830 | } 831 | 832 | fn set_href_prefix(&mut self) { 833 | match &mut self.things { 834 | ThingsType::Multiple(ref mut things, _) => { 835 | for (idx, thing) in things.iter_mut().enumerate() { 836 | let mut thing = thing.write().unwrap(); 837 | thing.set_href_prefix(format!("{}/{}", self.base_path, idx)); 838 | } 839 | } 840 | ThingsType::Single(ref mut thing) => { 841 | thing 842 | .write() 843 | .unwrap() 844 | .set_href_prefix(self.base_path.clone()); 845 | } 846 | } 847 | } 848 | 849 | /// Return the base actix configuration for the server 850 | /// useful for testing. 851 | pub fn make_config(&mut self) -> impl Fn(&mut web::ServiceConfig) + Clone + 'static { 852 | let port = self.port.unwrap_or(80); 853 | 854 | let mut hosts = vec!["localhost".to_owned(), format!("localhost:{}", port)]; 855 | 856 | if let Ok(system_hostname) = hostname::get() { 857 | let name = system_hostname.into_string().unwrap().to_lowercase(); 858 | hosts.push(format!("{}.local", name)); 859 | hosts.push(format!("{}.local:{}", name, port)); 860 | } 861 | 862 | for address in get_addresses() { 863 | hosts.push(address.clone()); 864 | hosts.push(format!("{}:{}", address, port)); 865 | } 866 | 867 | if let Some(ref hostname) = self.hostname { 868 | let name = hostname.to_lowercase(); 869 | hosts.push(name.clone()); 870 | hosts.push(format!("{}:{}", name, port)); 871 | } 872 | 873 | self.set_href_prefix(); 874 | 875 | let single = match &self.things { 876 | ThingsType::Multiple(_, _) => false, 877 | ThingsType::Single(_) => true, 878 | }; 879 | let things_arc = Arc::new(self.things.clone()); 880 | let hosts_arc = Arc::new(hosts.clone()); 881 | let generator_arc_clone = self.generator_arc.clone(); 882 | let disable_host_validation_arc = Arc::new(self.disable_host_validation); 883 | 884 | let bp = self.base_path.clone(); 885 | 886 | move |app: &mut web::ServiceConfig| { 887 | app.app_data(Data::new(AppState { 888 | things: things_arc.clone(), 889 | hosts: hosts_arc.clone(), 890 | disable_host_validation: disable_host_validation_arc.clone(), 891 | action_generator: generator_arc_clone.clone(), 892 | })); 893 | 894 | if single { 895 | let root = if bp.is_empty() { 896 | "/".to_owned() 897 | } else { 898 | bp.clone() 899 | }; 900 | 901 | app.service( 902 | web::resource(&root) 903 | .route( 904 | web::route() 905 | .guard(guard::Get()) 906 | .guard(guard::Header("upgrade", "websocket")) 907 | .to(handle_ws_thing), 908 | ) 909 | .route(web::get().to(handle_get_thing)), 910 | ) 911 | .service( 912 | web::resource(&format!("{}/properties", bp)) 913 | .route(web::get().to(handle_get_properties)), 914 | ) 915 | .service( 916 | web::resource(&format!("{}/properties/{{property_name}}", bp)) 917 | .route(web::get().to(handle_get_property)) 918 | .route(web::put().to(handle_put_property)), 919 | ) 920 | .service( 921 | web::resource(&format!("{}/actions", bp)) 922 | .route(web::get().to(handle_get_actions)) 923 | .route(web::post().to(handle_post_actions)), 924 | ) 925 | .service( 926 | web::resource(&format!("{}/actions/{{action_name}}", bp)) 927 | .route(web::get().to(handle_get_action)) 928 | .route(web::post().to(handle_post_action)), 929 | ) 930 | .service( 931 | web::resource(&format!("{}/actions/{{action_name}}/{{action_id}}", bp)) 932 | .route(web::get().to(handle_get_action_id)) 933 | .route(web::delete().to(handle_delete_action_id)) 934 | .route(web::put().to(handle_put_action_id)), 935 | ) 936 | .service( 937 | web::resource(&format!("{}/events", bp)) 938 | .route(web::get().to(handle_get_events)), 939 | ) 940 | .service( 941 | web::resource(&format!("{}/events/{{event_name}}", bp)) 942 | .route(web::get().to(handle_get_event)), 943 | ); 944 | } else { 945 | app.service(web::resource("/").route(web::get().to(handle_get_things))) 946 | .service( 947 | web::scope(&format!("{}/{{thing_id}}", bp)) 948 | .service( 949 | web::resource("") 950 | .route( 951 | web::route() 952 | .guard(guard::Get()) 953 | .guard(guard::Header("upgrade", "websocket")) 954 | .to(handle_ws_thing), 955 | ) 956 | .route(web::get().to(handle_get_thing)), 957 | ) 958 | .service( 959 | web::resource("/properties") 960 | .route(web::get().to(handle_get_properties)), 961 | ) 962 | .service( 963 | web::resource("/properties/{property_name}") 964 | .route(web::get().to(handle_get_property)) 965 | .route(web::put().to(handle_put_property)), 966 | ) 967 | .service( 968 | web::resource("/actions") 969 | .route(web::get().to(handle_get_actions)) 970 | .route(web::post().to(handle_post_actions)), 971 | ) 972 | .service( 973 | web::resource("/actions/{action_name}") 974 | .route(web::get().to(handle_get_action)) 975 | .route(web::post().to(handle_post_action)), 976 | ) 977 | .service( 978 | web::resource("/actions/{action_name}/{action_id}") 979 | .route(web::get().to(handle_get_action_id)) 980 | .route(web::delete().to(handle_delete_action_id)) 981 | .route(web::put().to(handle_put_action_id)), 982 | ) 983 | .service( 984 | web::resource("/events").route(web::get().to(handle_get_events)), 985 | ) 986 | .service( 987 | web::resource("/events/{event_name}") 988 | .route(web::get().to(handle_get_event)), 989 | ), 990 | ); 991 | } 992 | } 993 | } 994 | 995 | /// Start listening for incoming connections. 996 | pub fn start( 997 | &mut self, 998 | configure: Option<&'static (dyn Fn(&mut web::ServiceConfig) + Send + Sync + 'static)>, 999 | ) -> Server { 1000 | let port = self.port.unwrap_or(80); 1001 | 1002 | let name = match &self.things { 1003 | ThingsType::Single(thing) => thing.read().unwrap().get_title(), 1004 | ThingsType::Multiple(_, name) => name.to_owned(), 1005 | }; 1006 | 1007 | let things_config = self.make_config(); 1008 | 1009 | let server = HttpServer::new(move || { 1010 | let app = App::new() 1011 | .wrap(middleware::Logger::default()) 1012 | .wrap(HostValidator) 1013 | .wrap( 1014 | middleware::DefaultHeaders::new() 1015 | .add(("Access-Control-Allow-Origin", "*")) 1016 | .add(( 1017 | "Access-Control-Allow-Methods", 1018 | "GET, HEAD, PUT, POST, DELETE, OPTIONS", 1019 | )) 1020 | .add(( 1021 | "Access-Control-Allow-Headers", 1022 | "Origin, Content-Type, Accept, X-Requested-With", 1023 | )), 1024 | ) 1025 | .configure(&things_config); 1026 | 1027 | if let Some(ref configure) = configure { 1028 | app.configure(configure) 1029 | } else { 1030 | app 1031 | } 1032 | }); 1033 | 1034 | let responder = libmdns::Responder::new().unwrap(); 1035 | 1036 | #[cfg(feature = "ssl")] 1037 | match self.ssl_options { 1038 | Some(ref o) => { 1039 | self.dns_service = Some(responder.register( 1040 | SERVICE_TYPE.to_owned(), 1041 | name.clone(), 1042 | port, 1043 | &["path=/", "tls=1"], 1044 | )); 1045 | 1046 | let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); 1047 | builder 1048 | .set_private_key_file(o.0.clone(), SslFiletype::PEM) 1049 | .unwrap(); 1050 | builder.set_certificate_chain_file(o.1.clone()).unwrap(); 1051 | server 1052 | .bind_openssl(format!("0.0.0.0:{}", port), builder) 1053 | .expect("Failed to bind socket") 1054 | .run() 1055 | } 1056 | None => { 1057 | self.dns_service = Some(responder.register( 1058 | SERVICE_TYPE.to_owned(), 1059 | name.clone(), 1060 | port, 1061 | &["path=/"], 1062 | )); 1063 | server 1064 | .bind(format!("0.0.0.0:{}", port)) 1065 | .expect("Failed to bind socket") 1066 | .run() 1067 | } 1068 | } 1069 | 1070 | #[cfg(not(feature = "ssl"))] 1071 | { 1072 | self.dns_service = 1073 | Some(responder.register(SERVICE_TYPE.to_owned(), name, port, &["path=/"])); 1074 | server 1075 | .bind(format!("0.0.0.0:{}", port)) 1076 | .expect("Failed to bind socket") 1077 | .run() 1078 | } 1079 | } 1080 | } 1081 | -------------------------------------------------------------------------------- /src/thing.rs: -------------------------------------------------------------------------------- 1 | use serde_json; 2 | use serde_json::json; 3 | use std::any::Any; 4 | use std::collections::HashMap; 5 | use std::marker::{Send, Sync}; 6 | use std::sync::{Arc, RwLock}; 7 | use std::vec::Drain; 8 | use valico::json_schema; 9 | 10 | use super::action::Action; 11 | use super::event::Event; 12 | use super::property::Property; 13 | 14 | /// High-level Thing trait. 15 | pub trait Thing: Send + Sync { 16 | /// Return the thing state as a Thing Description. 17 | fn as_thing_description(&self) -> serde_json::Map; 18 | 19 | /// Return this thing as an Any. 20 | fn as_any(&self) -> &dyn Any; 21 | 22 | /// Return this thing as a mutable Any. 23 | fn as_mut_any(&mut self) -> &mut dyn Any; 24 | 25 | /// Get this thing's href. 26 | fn get_href(&self) -> String; 27 | 28 | /// Get this thing's href prefix, i.e. /0. 29 | fn get_href_prefix(&self) -> String; 30 | 31 | /// Get the UI href. 32 | fn get_ui_href(&self) -> Option; 33 | 34 | /// Set the prefix of any hrefs associated with this thing. 35 | fn set_href_prefix(&mut self, prefix: String); 36 | 37 | /// Set the href of this thing's custom UI. 38 | fn set_ui_href(&mut self, href: String); 39 | 40 | /// Get the ID of the thing. 41 | fn get_id(&self) -> String; 42 | 43 | /// Get the title of the thing. 44 | fn get_title(&self) -> String; 45 | 46 | /// Get the type context of the thing. 47 | /// It can be a plain string or an array of strings 48 | /// and maps namespace -> uri 49 | fn get_context(&self) -> serde_json::Value; 50 | 51 | /// Get the type(s) of the thing. 52 | fn get_type(&self) -> Vec; 53 | 54 | /// Get the description of the thing. 55 | fn get_description(&self) -> String; 56 | 57 | /// Get the thing's properties as a JSON map. 58 | /// 59 | /// Returns the properties as a JSON map, i.e. name -> description. 60 | fn get_property_descriptions(&self) -> serde_json::Map; 61 | 62 | /// Get the thing's actions as an array. 63 | fn get_action_descriptions(&self, action_name: Option) -> serde_json::Value; 64 | 65 | /// Get the thing's events as an array. 66 | fn get_event_descriptions(&self, event_name: Option) -> serde_json::Value; 67 | 68 | /// Add a property to this thing. 69 | fn add_property(&mut self, property: Box); 70 | 71 | /// Remove a property from this thing. 72 | fn remove_property(&mut self, property_name: &str); 73 | 74 | /// Find a property by name. 75 | fn find_property(&mut self, property_name: &str) -> Option<&mut Box>; 76 | 77 | /// Get a property's value. 78 | fn get_property(&self, property_name: &str) -> Option; 79 | 80 | /// Get a mapping of all properties and their values. 81 | /// 82 | /// Returns an object of propertyName -> value. 83 | fn get_properties(&self) -> serde_json::Map; 84 | 85 | /// Determine whether or not this thing has a given property. 86 | fn has_property(&self, property_name: &str) -> bool; 87 | 88 | /// Set a property value. 89 | fn set_property( 90 | &mut self, 91 | property_name: String, 92 | value: serde_json::Value, 93 | ) -> Result<(), &'static str> { 94 | let property = self 95 | .find_property(&property_name) 96 | .ok_or("Property not found")?; 97 | 98 | property.set_value(value.clone())?; 99 | self.property_notify(property_name, value); 100 | 101 | Ok(()) 102 | } 103 | 104 | /// Get an action. 105 | fn get_action( 106 | &self, 107 | action_name: String, 108 | action_id: String, 109 | ) -> Option>>>; 110 | 111 | /// Add a new event and notify subscribers. 112 | fn add_event(&mut self, event: Box); 113 | 114 | /// Add an available event. 115 | /// 116 | /// # Arguments 117 | /// 118 | /// * `name` - name of the event 119 | /// * `metadata` - event metadata, i.e. type, description, etc., as a JSON map 120 | fn add_available_event( 121 | &mut self, 122 | name: String, 123 | metadata: serde_json::Map, 124 | ); 125 | 126 | /// Perform an action on the thing. 127 | /// 128 | /// Returns the action that was created. 129 | fn add_action( 130 | &mut self, 131 | action: Arc>>, 132 | input: Option<&serde_json::Value>, 133 | ) -> Result<(), &str>; 134 | 135 | /// Remove an existing action. 136 | /// 137 | /// Returns a boolean indicating the presence of the action. 138 | fn remove_action(&mut self, action_name: String, action_id: String) -> bool; 139 | 140 | /// Add an available action. 141 | /// 142 | /// # Arguments 143 | /// 144 | /// * `name` - name of the action 145 | /// * `metadata` - action metadata, i.e. type, description, etc., as a JSON map 146 | fn add_available_action( 147 | &mut self, 148 | name: String, 149 | metadata: serde_json::Map, 150 | ); 151 | 152 | /// Add a new websocket subscriber. 153 | /// 154 | /// # Arguments 155 | /// 156 | /// * `ws_id` - ID of the websocket 157 | fn add_subscriber(&mut self, ws_id: String); 158 | 159 | /// Remove a websocket subscriber. 160 | /// 161 | /// # Arguments 162 | /// 163 | /// * `ws_id` - ID of the websocket 164 | fn remove_subscriber(&mut self, ws_id: String); 165 | 166 | /// Add a new websocket subscriber to an event. 167 | /// 168 | /// # Arguments 169 | /// 170 | /// * `name` - name of the event 171 | /// * `ws_id` - ID of the websocket 172 | fn add_event_subscriber(&mut self, name: String, ws_id: String); 173 | 174 | /// Remove a websocket subscriber from an event. 175 | /// 176 | /// # Arguments 177 | /// 178 | /// * `name` - name of the event 179 | /// * `ws_id` - ID of the websocket 180 | fn remove_event_subscriber(&mut self, name: String, ws_id: String); 181 | 182 | /// Notify all subscribers of a property change. 183 | fn property_notify(&mut self, name: String, value: serde_json::Value); 184 | 185 | /// Notify all subscribers of an action status change. 186 | fn action_notify(&mut self, action: serde_json::Map); 187 | 188 | /// Notify all subscribers of an event. 189 | fn event_notify(&mut self, name: String, event: serde_json::Map); 190 | 191 | /// Start the specified action. 192 | fn start_action(&mut self, name: String, id: String); 193 | 194 | /// Cancel the specified action. 195 | fn cancel_action(&mut self, name: String, id: String); 196 | 197 | /// Finish the specified action. 198 | fn finish_action(&mut self, name: String, id: String); 199 | 200 | /// Drain any message queues for the specified weboscket ID. 201 | /// 202 | /// # Arguments 203 | /// 204 | /// * `ws_id` - ID of the websocket 205 | fn drain_queue(&mut self, ws_id: String) -> Vec>; 206 | } 207 | 208 | /// Vocabularies to be passed as json-ld @context 209 | /// 210 | /// The default is the plain `https://webthings.io/schemas` 211 | #[derive(Clone)] 212 | pub struct ThingContext { 213 | plain: Vec, 214 | namespaced: HashMap, 215 | } 216 | 217 | impl ThingContext { 218 | /// Create a plain json-ld context with a single entry 219 | pub fn new(base: &str) -> Self { 220 | ThingContext { 221 | plain: vec![base.to_owned()], 222 | namespaced: HashMap::new(), 223 | } 224 | } 225 | } 226 | 227 | impl std::iter::FromIterator<(Option, B)> for ThingContext 228 | where 229 | A: AsRef, 230 | B: AsRef, 231 | { 232 | /// Create a rich json-ld context from an iterable 233 | fn from_iter(iter: I) -> Self 234 | where 235 | I: IntoIterator, B)>, 236 | { 237 | let mut plain = Vec::new(); 238 | let mut namespaced = HashMap::new(); 239 | 240 | for (maybe_k, val) in iter { 241 | let val = val.as_ref().to_owned(); 242 | if let Some(k) = maybe_k { 243 | namespaced.insert(k.as_ref().to_owned(), val); 244 | } else { 245 | plain.push(val); 246 | } 247 | } 248 | 249 | ThingContext { plain, namespaced } 250 | } 251 | } 252 | 253 | impl Default for ThingContext { 254 | /// Plain `https://webthings.io/schemas` vocabulary 255 | fn default() -> Self { 256 | ThingContext::new("https://webthings.io/schemas") 257 | } 258 | } 259 | 260 | /// Basic web thing implementation. 261 | /// 262 | /// This can easily be used by other things to handle most of the boring work. 263 | #[derive(Default)] 264 | pub struct BaseThing { 265 | id: String, 266 | context: ThingContext, 267 | type_: Vec, 268 | title: String, 269 | description: String, 270 | properties: HashMap>, 271 | available_actions: HashMap, 272 | available_events: HashMap, 273 | actions: HashMap>>>>, 274 | events: Vec>, 275 | subscribers: HashMap>, 276 | href_prefix: String, 277 | ui_href: Option, 278 | } 279 | 280 | impl BaseThing { 281 | /// Create a new BaseThing. 282 | /// 283 | /// # Arguments 284 | /// 285 | /// * `id` - the thing's unique ID - must be a URI 286 | /// * `title` - the thing's title 287 | /// * `type_` - the thing's type(s) 288 | /// * `description` - description of the thing 289 | /// * `context` - vocabularies to be used in the thing description 290 | pub fn new( 291 | id: String, 292 | title: String, 293 | type_: Option>, 294 | description: Option, 295 | ) -> Self { 296 | Self { 297 | id, 298 | type_: type_.unwrap_or_else(Vec::new), 299 | title, 300 | description: description.unwrap_or_else(|| "".to_string()), 301 | ..Default::default() 302 | } 303 | } 304 | 305 | /// Overwrite the Thing Description context with a new set of 306 | /// vocabularies. 307 | pub fn with_context(mut self, context: ThingContext) -> Self { 308 | self.context = context; 309 | self 310 | } 311 | } 312 | 313 | impl Thing for BaseThing { 314 | /// Return the thing state as a Thing Description. 315 | fn as_thing_description(&self) -> serde_json::Map { 316 | let mut description = serde_json::Map::new(); 317 | 318 | description.insert("id".to_owned(), json!(self.get_id())); 319 | description.insert("title".to_owned(), json!(self.get_title())); 320 | description.insert("@context".to_owned(), self.get_context()); 321 | description.insert("@type".to_owned(), json!(self.get_type())); 322 | description.insert( 323 | "properties".to_owned(), 324 | json!(self.get_property_descriptions()), 325 | ); 326 | 327 | let mut links: Vec> = Vec::new(); 328 | 329 | let mut properties_link = serde_json::Map::new(); 330 | properties_link.insert("rel".to_owned(), json!("properties")); 331 | properties_link.insert( 332 | "href".to_owned(), 333 | json!(format!("{}/properties", self.get_href_prefix())), 334 | ); 335 | links.push(properties_link); 336 | 337 | let mut actions_link = serde_json::Map::new(); 338 | actions_link.insert("rel".to_owned(), json!("actions")); 339 | actions_link.insert( 340 | "href".to_owned(), 341 | json!(format!("{}/actions", self.get_href_prefix())), 342 | ); 343 | links.push(actions_link); 344 | 345 | let mut events_link = serde_json::Map::new(); 346 | events_link.insert("rel".to_owned(), json!("events")); 347 | events_link.insert( 348 | "href".to_owned(), 349 | json!(format!("{}/events", self.get_href_prefix())), 350 | ); 351 | links.push(events_link); 352 | 353 | if let Some(ui_href) = self.get_ui_href() { 354 | let mut ui_link = serde_json::Map::new(); 355 | ui_link.insert("rel".to_owned(), json!("alternate")); 356 | ui_link.insert("mediaType".to_owned(), json!("text/html")); 357 | ui_link.insert("href".to_owned(), json!(ui_href)); 358 | links.push(ui_link); 359 | } 360 | 361 | description.insert("links".to_owned(), json!(links)); 362 | 363 | let mut actions = serde_json::Map::new(); 364 | for (name, action) in self.available_actions.iter() { 365 | let mut metadata = action.get_metadata().clone(); 366 | metadata.insert( 367 | "links".to_string(), 368 | json!([ 369 | { 370 | "rel": "action", 371 | "href": format!("{}/actions/{}", self.get_href_prefix(), name), 372 | }, 373 | ]), 374 | ); 375 | actions.insert(name.to_string(), json!(metadata)); 376 | } 377 | 378 | description.insert("actions".to_owned(), json!(actions)); 379 | 380 | let mut events = serde_json::Map::new(); 381 | for (name, event) in self.available_events.iter() { 382 | let mut metadata = event.get_metadata().clone(); 383 | metadata.insert( 384 | "links".to_string(), 385 | json!([ 386 | { 387 | "rel": "event", 388 | "href": format!("{}/events/{}", self.get_href_prefix(), name), 389 | }, 390 | ]), 391 | ); 392 | events.insert(name.to_string(), json!(metadata)); 393 | } 394 | 395 | description.insert("events".to_owned(), json!(events)); 396 | 397 | if !self.description.is_empty() { 398 | description.insert("description".to_owned(), json!(self.description)); 399 | } 400 | 401 | description 402 | } 403 | 404 | /// Return this thing as an Any. 405 | fn as_any(&self) -> &dyn Any { 406 | self 407 | } 408 | 409 | /// Return this thing as a mutable Any. 410 | fn as_mut_any(&mut self) -> &mut dyn Any { 411 | self 412 | } 413 | 414 | /// Get this thing's href. 415 | fn get_href(&self) -> String { 416 | if self.href_prefix.is_empty() { 417 | "/".to_owned() 418 | } else { 419 | self.href_prefix.clone() 420 | } 421 | } 422 | 423 | /// Get this thing's href prefix, i.e. /0. 424 | fn get_href_prefix(&self) -> String { 425 | self.href_prefix.clone() 426 | } 427 | 428 | /// Get the UI href. 429 | fn get_ui_href(&self) -> Option { 430 | self.ui_href.clone() 431 | } 432 | 433 | /// Set the prefix of any hrefs associated with this thing. 434 | fn set_href_prefix(&mut self, prefix: String) { 435 | self.href_prefix = prefix.clone(); 436 | 437 | for property in self.properties.values_mut() { 438 | property.set_href_prefix(prefix.clone()); 439 | } 440 | 441 | for actions in self.actions.values_mut() { 442 | for action in actions { 443 | action.write().unwrap().set_href_prefix(prefix.clone()); 444 | } 445 | } 446 | } 447 | 448 | /// Set the href of this thing's custom UI. 449 | fn set_ui_href(&mut self, href: String) { 450 | self.ui_href = Some(href); 451 | } 452 | 453 | /// Get the ID of the thing. 454 | fn get_id(&self) -> String { 455 | self.id.clone() 456 | } 457 | 458 | /// Get the title of the thing. 459 | fn get_title(&self) -> String { 460 | self.title.clone() 461 | } 462 | 463 | /// Get the type context of the thing. 464 | fn get_context(&self) -> serde_json::Value { 465 | if self.context.namespaced.is_empty() { 466 | if self.context.plain.len() == 1 { 467 | json!(self.context.plain[0]) 468 | } else { 469 | json!(self.context.plain) 470 | } 471 | } else { 472 | let mut values = json!(self.context.plain); 473 | if let Some(v) = values.as_array_mut() { 474 | v.push(json!(self.context.namespaced)) 475 | } 476 | values 477 | } 478 | } 479 | 480 | /// Get the type(s) of the thing. 481 | fn get_type(&self) -> Vec { 482 | self.type_.clone() 483 | } 484 | 485 | /// Get the description of the thing. 486 | fn get_description(&self) -> String { 487 | self.description.clone() 488 | } 489 | 490 | /// Get the thing's properties as a JSON map. 491 | /// 492 | /// Returns the properties as a JSON map, i.e. name -> description. 493 | fn get_property_descriptions(&self) -> serde_json::Map { 494 | let mut descriptions = serde_json::Map::new(); 495 | 496 | for (name, property) in self.properties.iter() { 497 | descriptions.insert(name.to_string(), json!(property.as_property_description())); 498 | } 499 | 500 | descriptions 501 | } 502 | 503 | /// Get the thing's actions as an array. 504 | fn get_action_descriptions(&self, action_name: Option) -> serde_json::Value { 505 | let mut descriptions = Vec::new(); 506 | 507 | match action_name { 508 | Some(action_name) => { 509 | if let Some(actions) = self.actions.get(&action_name) { 510 | for action in actions { 511 | descriptions.push(action.read().unwrap().as_action_description()); 512 | } 513 | } 514 | } 515 | None => { 516 | for action in self.actions.values().flatten() { 517 | descriptions.push(action.read().unwrap().as_action_description()); 518 | } 519 | } 520 | } 521 | 522 | json!(descriptions) 523 | } 524 | 525 | /// Get the thing's events as an array. 526 | fn get_event_descriptions(&self, event_name: Option) -> serde_json::Value { 527 | let mut descriptions = Vec::new(); 528 | 529 | match event_name { 530 | Some(event_name) => { 531 | for event in &self.events { 532 | if event.get_name() == event_name { 533 | descriptions.push(event.as_event_description()); 534 | } 535 | } 536 | } 537 | None => { 538 | for event in &self.events { 539 | descriptions.push(event.as_event_description()); 540 | } 541 | } 542 | } 543 | 544 | json!(descriptions) 545 | } 546 | 547 | /// Add a property to this thing. 548 | fn add_property(&mut self, mut property: Box) { 549 | property.set_href_prefix(self.get_href_prefix()); 550 | self.properties.insert(property.get_name(), property); 551 | } 552 | 553 | /// Remove a property from this thing. 554 | fn remove_property(&mut self, property_name: &str) { 555 | self.properties.remove(property_name); 556 | } 557 | 558 | /// Find a property by name. 559 | fn find_property(&mut self, property_name: &str) -> Option<&mut Box> { 560 | self.properties.get_mut(property_name) 561 | } 562 | 563 | /// Get a property's value. 564 | fn get_property(&self, property_name: &str) -> Option { 565 | self.properties.get(property_name).map(|p| p.get_value()) 566 | } 567 | 568 | /// Get a mapping of all properties and their values. 569 | /// 570 | /// Returns an object of propertyName -> value. 571 | fn get_properties(&self) -> serde_json::Map { 572 | let mut properties = serde_json::Map::new(); 573 | for (name, property) in self.properties.iter() { 574 | properties.insert(name.to_string(), json!(property.get_value())); 575 | } 576 | properties 577 | } 578 | 579 | /// Determine whether or not this thing has a given property. 580 | fn has_property(&self, property_name: &str) -> bool { 581 | self.properties.contains_key(property_name) 582 | } 583 | 584 | /// Get an action. 585 | fn get_action( 586 | &self, 587 | action_name: String, 588 | action_id: String, 589 | ) -> Option>>> { 590 | match self.actions.get(&action_name) { 591 | Some(entry) => { 592 | for action in entry { 593 | if action.read().unwrap().get_id() == action_id { 594 | return Some(action.clone()); 595 | } 596 | } 597 | 598 | None 599 | } 600 | None => None, 601 | } 602 | } 603 | 604 | /// Add a new event and notify subscribers. 605 | fn add_event(&mut self, event: Box) { 606 | self.event_notify(event.get_name(), event.as_event_description()); 607 | self.events.push(event); 608 | } 609 | 610 | /// Add an available event. 611 | /// 612 | /// # Arguments 613 | /// 614 | /// * `name` - name of the event 615 | /// * `metadata` - event metadata, i.e. type, description, etc., as a JSON map 616 | fn add_available_event( 617 | &mut self, 618 | name: String, 619 | metadata: serde_json::Map, 620 | ) { 621 | let event = AvailableEvent::new(metadata); 622 | self.available_events.insert(name, event); 623 | } 624 | 625 | /// Perform an action on the thing. 626 | /// 627 | /// Returns the action that was created. 628 | fn add_action( 629 | &mut self, 630 | action: Arc>>, 631 | input: Option<&serde_json::Value>, 632 | ) -> Result<(), &str> { 633 | let action_name = action.read().unwrap().get_name(); 634 | 635 | if let Some(action_type) = self.available_actions.get(&action_name) { 636 | if !action_type.validate_action_input(input) { 637 | return Err("Action input invalid"); 638 | } 639 | } else { 640 | return Err("Action type not found"); 641 | } 642 | 643 | action 644 | .write() 645 | .unwrap() 646 | .set_href_prefix(self.get_href_prefix()); 647 | self.action_notify(action.read().unwrap().as_action_description()); 648 | self.actions.get_mut(&action_name).unwrap().push(action); 649 | 650 | Ok(()) 651 | } 652 | 653 | /// Remove an existing action. 654 | /// 655 | /// Returns a boolean indicating the presence of the action. 656 | fn remove_action(&mut self, action_name: String, action_id: String) -> bool { 657 | let action = self.get_action(action_name.clone(), action_id.clone()); 658 | match action { 659 | Some(action) => { 660 | action.write().unwrap().cancel(); 661 | 662 | let actions = self.actions.get_mut(&action_name).unwrap(); 663 | actions.retain(|a| a.read().unwrap().get_id() != action_id); 664 | 665 | true 666 | } 667 | None => false, 668 | } 669 | } 670 | 671 | /// Add an available action. 672 | /// 673 | /// # Arguments 674 | /// 675 | /// * `name` - name of the action 676 | /// * `metadata` - action metadata, i.e. type, description, etc., as a JSON map 677 | fn add_available_action( 678 | &mut self, 679 | name: String, 680 | metadata: serde_json::Map, 681 | ) { 682 | let action = AvailableAction::new(metadata); 683 | self.available_actions.insert(name.clone(), action); 684 | self.actions.insert(name, Vec::new()); 685 | } 686 | 687 | /// Add a new websocket subscriber. 688 | /// 689 | /// # Arguments 690 | /// 691 | /// * `ws_id` - ID of the websocket 692 | fn add_subscriber(&mut self, ws_id: String) { 693 | self.subscribers.insert(ws_id, Vec::new()); 694 | } 695 | 696 | /// Remove a websocket subscriber. 697 | /// 698 | /// # Arguments 699 | /// 700 | /// * `ws_id` - ID of the websocket 701 | fn remove_subscriber(&mut self, ws_id: String) { 702 | self.subscribers.remove(&ws_id); 703 | 704 | for event in self.available_events.values_mut() { 705 | event.remove_subscriber(ws_id.clone()); 706 | } 707 | } 708 | 709 | /// Add a new websocket subscriber to an event. 710 | /// 711 | /// # Arguments 712 | /// 713 | /// * `name` - name of the event 714 | /// * `ws_id` - ID of the websocket 715 | fn add_event_subscriber(&mut self, name: String, ws_id: String) { 716 | if let Some(event) = self.available_events.get_mut(&name) { 717 | event.add_subscriber(ws_id); 718 | } 719 | } 720 | 721 | /// Remove a websocket subscriber from an event. 722 | /// 723 | /// # Arguments 724 | /// 725 | /// * `name` - name of the event 726 | /// * `ws_id` - ID of the websocket 727 | fn remove_event_subscriber(&mut self, name: String, ws_id: String) { 728 | if let Some(event) = self.available_events.get_mut(&name) { 729 | event.remove_subscriber(ws_id); 730 | } 731 | } 732 | 733 | /// Notify all subscribers of a property change. 734 | fn property_notify(&mut self, name: String, value: serde_json::Value) { 735 | let message = json!({ 736 | "messageType": "propertyStatus", 737 | "data": { 738 | name: value 739 | } 740 | }) 741 | .to_string(); 742 | 743 | self.subscribers 744 | .values_mut() 745 | .for_each(|queue| queue.push(message.clone())); 746 | } 747 | 748 | /// Notify all subscribers of an action status change. 749 | fn action_notify(&mut self, action: serde_json::Map) { 750 | let message = json!({ 751 | "messageType": "actionStatus", 752 | "data": action 753 | }) 754 | .to_string(); 755 | 756 | self.subscribers 757 | .values_mut() 758 | .for_each(|queue| queue.push(message.clone())); 759 | } 760 | 761 | /// Notify all subscribers of an event. 762 | fn event_notify(&mut self, name: String, event: serde_json::Map) { 763 | if !self.available_events.contains_key(&name) { 764 | return; 765 | } 766 | 767 | let message = json!({ 768 | "messageType": "event", 769 | "data": event, 770 | }) 771 | .to_string(); 772 | 773 | self.available_events 774 | .get_mut(&name) 775 | .unwrap() 776 | .get_subscribers() 777 | .values_mut() 778 | .for_each(|queue| queue.push(message.clone())); 779 | } 780 | 781 | /// Start the specified action. 782 | fn start_action(&mut self, name: String, id: String) { 783 | if let Some(action) = self.get_action(name, id) { 784 | let mut a = action.write().unwrap(); 785 | a.start(); 786 | self.action_notify(a.as_action_description()); 787 | a.perform_action(); 788 | } 789 | } 790 | 791 | /// Cancel the specified action. 792 | fn cancel_action(&mut self, name: String, id: String) { 793 | if let Some(action) = self.get_action(name, id) { 794 | let mut a = action.write().unwrap(); 795 | a.cancel(); 796 | } 797 | } 798 | 799 | /// Finish the specified action. 800 | fn finish_action(&mut self, name: String, id: String) { 801 | if let Some(action) = self.get_action(name, id) { 802 | let mut a = action.write().unwrap(); 803 | a.finish(); 804 | self.action_notify(a.as_action_description()); 805 | } 806 | } 807 | 808 | /// Drain any message queues for the specified weboscket ID. 809 | /// 810 | /// 811 | /// # Arguments 812 | /// 813 | /// * `ws_id` - ID of the websocket 814 | fn drain_queue(&mut self, ws_id: String) -> Vec> { 815 | let mut drains: Vec> = Vec::new(); 816 | if let Some(v) = self.subscribers.get_mut(&ws_id) { 817 | drains.push(v.drain(..)); 818 | } 819 | 820 | self.available_events.values_mut().for_each(|evt| { 821 | if let Some(v) = evt.get_subscribers().get_mut(&ws_id) { 822 | drains.push(v.drain(..)); 823 | } 824 | }); 825 | 826 | drains 827 | } 828 | } 829 | 830 | /// Struct to describe an action available to be taken. 831 | struct AvailableAction { 832 | metadata: serde_json::Map, 833 | } 834 | 835 | impl AvailableAction { 836 | /// Create a new AvailableAction. 837 | /// 838 | /// # Arguments 839 | /// 840 | /// * `metadata` - action metadata 841 | fn new(metadata: serde_json::Map) -> AvailableAction { 842 | AvailableAction { metadata } 843 | } 844 | 845 | /// Get the action metadata. 846 | fn get_metadata(&self) -> &serde_json::Map { 847 | &self.metadata 848 | } 849 | 850 | /// Validate the input for a new action. 851 | /// 852 | /// Returns a boolean indicating validation success. 853 | fn validate_action_input(&self, input: Option<&serde_json::Value>) -> bool { 854 | let mut scope = json_schema::Scope::new(); 855 | let validator = if let Some(input) = self.metadata.get("input") { 856 | let mut schema = input.as_object().unwrap().clone(); 857 | if let Some(properties) = schema.get_mut("properties") { 858 | let properties = properties.as_object_mut().unwrap(); 859 | for value in properties.values_mut() { 860 | let value = value.as_object_mut().unwrap(); 861 | value.remove("@type"); 862 | value.remove("unit"); 863 | value.remove("title"); 864 | } 865 | } 866 | 867 | match scope.compile_and_return(json!(schema), true) { 868 | Ok(s) => Some(s), 869 | Err(_) => None, 870 | } 871 | } else { 872 | None 873 | }; 874 | 875 | match validator { 876 | Some(ref v) => match input { 877 | Some(i) => v.validate(i).is_valid(), 878 | None => v.validate(&serde_json::Value::Null).is_valid(), 879 | }, 880 | None => true, 881 | } 882 | } 883 | } 884 | 885 | /// Struct to describe an event available for subscription. 886 | struct AvailableEvent { 887 | metadata: serde_json::Map, 888 | subscribers: HashMap>, 889 | } 890 | 891 | impl AvailableEvent { 892 | /// Create a new AvailableEvent. 893 | /// 894 | /// # Arguments 895 | /// 896 | /// * `metadata` - event metadata 897 | fn new(metadata: serde_json::Map) -> AvailableEvent { 898 | AvailableEvent { 899 | metadata, 900 | subscribers: HashMap::new(), 901 | } 902 | } 903 | 904 | /// Get the event metadata. 905 | fn get_metadata(&self) -> &serde_json::Map { 906 | &self.metadata 907 | } 908 | 909 | /// Add a websocket subscriber to the event. 910 | /// 911 | /// # Arguments 912 | /// 913 | /// * `ws_id` - ID of the websocket 914 | fn add_subscriber(&mut self, ws_id: String) { 915 | self.subscribers.insert(ws_id, Vec::new()); 916 | } 917 | 918 | /// Remove a websocket subscriber from the event. 919 | /// 920 | /// # Arguments 921 | /// 922 | /// * `ws_id` - ID of the websocket 923 | fn remove_subscriber(&mut self, ws_id: String) { 924 | self.subscribers.remove(&ws_id); 925 | } 926 | 927 | /// Get the set of subscribers for the event. 928 | fn get_subscribers(&mut self) -> &mut HashMap> { 929 | &mut self.subscribers 930 | } 931 | } 932 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use chrono::Utc; 2 | 3 | #[cfg(feature = "actix")] 4 | use std::{collections::HashSet, net::IpAddr}; 5 | 6 | /// Get the current time. 7 | /// 8 | /// Returns the current time in the form YYYY-mm-ddTHH:MM:SS+00:00 9 | pub fn timestamp() -> String { 10 | let now = Utc::now(); 11 | now.format("%Y-%m-%dT%H:%M:%S+00:00").to_string() 12 | } 13 | 14 | /// Get all IP addresses 15 | #[cfg(feature = "actix")] 16 | pub fn get_addresses() -> Vec { 17 | let mut addresses = HashSet::new(); 18 | 19 | for iface in if_addrs::get_if_addrs().unwrap() { 20 | match iface.ip() { 21 | IpAddr::V4(addr) => addresses.insert(addr.to_string()), 22 | IpAddr::V6(addr) => addresses.insert(format!("[{}]", addr.to_string())), 23 | }; 24 | } 25 | 26 | let mut results = Vec::with_capacity(addresses.len()); 27 | results.extend(addresses.into_iter()); 28 | results.sort_unstable(); 29 | 30 | results 31 | } 32 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # build library 4 | cargo build 5 | cargo build --features ssl 6 | 7 | # clone the webthing-tester 8 | if [ ! -d webthing-tester ]; then 9 | git clone https://github.com/WebThingsIO/webthing-tester 10 | fi 11 | pip3 install --user -r webthing-tester/requirements.txt 12 | 13 | # build and test the single-thing example 14 | cargo build --example single-thing 15 | cargo run --example single-thing & 16 | EXAMPLE_PID=$! 17 | sleep 5 18 | ./webthing-tester/test-client.py 19 | kill -15 $EXAMPLE_PID 20 | 21 | # build and test the multiple-things example 22 | cargo build --example multiple-things 23 | cargo run --example multiple-things & 24 | EXAMPLE_PID=$! 25 | sleep 5 26 | ./webthing-tester/test-client.py --path-prefix "/0" 27 | kill -15 $EXAMPLE_PID 28 | --------------------------------------------------------------------------------